Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fixed contact us form position to maintain structure on bigger displays
- Fixed non-interactive form input field bug on Contact Us page
- Bumped Next.js from v15.3.2 to v15.3.8 to fix React server component's vulnerability
- Fixed XSS vulnerability and added client-side email format validation in NewsletterForm
- Escaped user inputs to prevent HTML injection
- Strengthened server-side validation to block malformed inputs before reCAPTCHA

### Changed

Expand Down
20 changes: 10 additions & 10 deletions components/NewsletterSubscribe/NewsletterForm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ const NewsletterForm = ({ getReCaptchaToken }) => {
return null;
}

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
setError('Please enter a valid email address');
return null;
}

const confirmValidateRecaptcha = await validateReCaptcha();
if (confirmValidateRecaptcha) {
setName('');
Expand Down Expand Up @@ -153,18 +159,12 @@ const NewsletterForm = ({ getReCaptchaToken }) => {
<div className={styles.formSending}>Sending...</div>
)}
{status === 'error' || error ? (
<div
className={styles.formError}
dangerouslySetInnerHTML={{
__html: error || getMessage(message),
}}
/>
<div className={styles.formError}>
{error || getMessage(message)}
</div>
) : null}
{status === 'success' && status !== 'error' && !error && (
<div
className={styles.formSuccess}
dangerouslySetInnerHTML={{ __html: decode(message) }}
/>
<div className={styles.formSuccess}>{decode(message)}</div>
)}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not directly related to your update, but the condition on lines 161-168 looks a bit off. Would this work?

{(status === 'error' || error) && (
  <div className={styles.formError}>
    {error || getMessage(message)}
  </div>
)}

{status === 'success' && !error && (
  <div className={styles.formSuccess}>
    {decode(message)}
  </div>
)}

</div>
</div>
Expand Down
13 changes: 7 additions & 6 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
});
const withPWA = require('next-pwa');

module.exports = withPWA({
pwa: {
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
},
compiler: {
styledComponents: {
ssr: true,
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
"dependencies": {
"@sendgrid/mail": "^8.1.5",
"html-entities": "^2.3.2",
"next": "15.3.8",
"next-pwa": "^5.6.0",
"next": "^15.5.14",
"next-pwa": "^2.0.2",
"node-mailjet": "^6.0.9",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-google-recaptcha": "^3.1.0",
"react-hook-form": "^7.35.0",
"sass": "^1.35.1",
"swiper": "^11.2.6"
"swiper": "^12.1.3"
},
"devDependencies": {
"husky": "^9.1.7",
Expand Down
16 changes: 11 additions & 5 deletions pages/api/sendEmail.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Sends email to hello@webdevpath.co when user submit the form in "Contact Us" page

import { Client } from 'node-mailjet';
import { encode } from 'html-entities';

const mailjet = new Client({
apiKey: process.env.MAILJET_API_KEY,
Expand All @@ -17,6 +18,11 @@ export default async (email, name, subject, message, subscribe) => {
const mailjetEmail = 'support@webdevpath.co';

try {
const safeName = encode(name);
const safeEmail = encode(email);
const safeSubject = encode(subject);
const safeMessage = encode(message);

const data = {
Messages: [
{
Expand All @@ -29,12 +35,12 @@ export default async (email, name, subject, message, subscribe) => {
Email: receiverEmail,
},
],
Subject: `New message from ${name} via webdevpath.co 'Contact Us' Form`,
Subject: `New message from ${safeName} via webdevpath.co 'Contact Us' Form`,
HTMLPart: `
<b>Name:</b> ${name} <br/>
<b>Email:</b> <a href='mailto:${email}'>${email}</a><br/><br/>
<u><b>Subject:</b> ${subject}</u><br/>
<b>Message:</b> ${message} <br/>
<b>Name:</b> ${safeName} <br/>
<b>Email:</b> <a href='mailto:${safeEmail}'>${safeEmail}</a><br/><br/>
<u><b>Subject:</b> ${safeSubject}</u><br/>
<b>Message:</b> ${safeMessage} <br/>
<b>Subscribe?:</b> ${subscribe ? 'Yes' : 'No'}
`,
},
Expand Down
22 changes: 22 additions & 0 deletions pages/api/validateReCaptcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ export default async function handler(req, res) {
});
}

// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(422).json({
message: 'Invalid email format',
});
}

// Validate name length (max 100 characters)
if (name.length > 100) {
return res.status(422).json({
message: 'Name must be 100 characters or less',
});
}

// Validate message length if provided (max 5000 characters)
if (message && message.length > 5000) {
return res.status(422).json({
message: 'Message must be 5000 characters or less',
});
}

try {
// Ping the google recaptcha verify API to verify the captcha code you received
const response = await fetch(
Expand Down
Loading