Skip to content

Commit 9f99384

Browse files
Merge pull request #303 from Web-Dev-Path/fix/security-input-validation
Fix/security input validation
2 parents f16ebf2 + 6205123 commit 9f99384

11 files changed

Lines changed: 1711 additions & 2553 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
147147

148148
### Added
149149

150+
- Added Zod validation schemas for newsletter and contact forms
150151
- Added Faith to 'about us'
151152
- Updated Mariana's title in 'about us'
152153
- Updated outdated dependencies
@@ -157,16 +158,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
157158

158159
### Fixed
159160

161+
- Fixed next-pwa import syntax for v2.x compatibility
160162
- Updated husky script to avoid warning
161163
- Resolved incorrect meta tag rendering for nested routes
162164
- Prevent horizontal page scroll caused by overflowing long titles
163165
- Fixed hero images' max-height to align with WDP mockup
164166
- Fixed contact us form position to maintain structure on bigger displays
165167
- Fixed non-interactive form input field bug on Contact Us page
166168
- Bumped Next.js from v15.3.2 to v15.3.8 to fix React server component's vulnerability
169+
- Fixed XSS vulnerability and added client-side email format validation in NewsletterForm
170+
- Escaped user inputs to prevent HTML injection
171+
- Strengthened server-side validation to block malformed inputs before reCAPTCHA
167172

168173
### Changed
169174

175+
- Replaced manual server-side validation in validateReCaptcha with shared Zod schemas
170176
- Migrating styles from Styled Components to CSS Modules
171177
- ContactUsCards
172178
- ContactUsForm

components/ContactUs/ContactUsForm/index.js

Lines changed: 21 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useForm } from 'react-hook-form';
2+
import { zodResolver } from '@hookform/resolvers/zod';
23
import Container from '@/components/containers/Container';
34
import RevealContentContainer from '@/components/containers/RevealContentContainer';
45
import { SubmitButton } from '@/components/buttons/SubmitButton';
6+
import { contactSchema } from '@/utils/schemas/contact';
57
import styles from './ContactUsForm.module.scss';
68

79
function ContactUsForm({ subject, setResponseMessage, getReCaptchaToken }) {
@@ -11,11 +13,12 @@ function ContactUsForm({ subject, setResponseMessage, getReCaptchaToken }) {
1113
reset,
1214
formState: { errors, isSubmitting },
1315
} = useForm({
16+
resolver: zodResolver(contactSchema),
1417
defaultValues: {
15-
Name: '',
16-
Email: '',
17-
Subject: subject || '',
18-
Message: '',
18+
name: '',
19+
email: '',
20+
subject: subject || '',
21+
message: '',
1922
},
2023
});
2124

@@ -33,11 +36,11 @@ function ContactUsForm({ subject, setResponseMessage, getReCaptchaToken }) {
3336
'Content-Type': 'application/json',
3437
},
3538
body: JSON.stringify({
36-
name: data.Name,
37-
email: data.Email,
38-
subject: data.Subject,
39-
message: data.Message,
40-
subscribe: data.Subscribe,
39+
name: data.name,
40+
email: data.email,
41+
subject: data.subject,
42+
message: data.message,
43+
subscribe: data.subscribe,
4144
gReCaptchaToken,
4245
}),
4346
});
@@ -65,78 +68,35 @@ function ContactUsForm({ subject, setResponseMessage, getReCaptchaToken }) {
6568
className={styles.input}
6669
type='text'
6770
placeholder='name'
68-
{...register('Name', {
69-
required: true,
70-
minLength: 2,
71-
maxLength: 80,
72-
//no white space pattern
73-
pattern: /[^\s-]/i,
74-
})}
71+
{...register('name')}
7572
/>
76-
<p className={styles['error-msg']}>
77-
{errors.Name?.type === 'required'
78-
? 'Name is required'
79-
: errors.Name?.type === 'pattern'
80-
? 'No whitespace'
81-
: errors.Name?.type === 'minLength'
82-
? 'Must be more than 1 character'
83-
: undefined}
84-
</p>
73+
<p className={styles['error-msg']}>{errors.name?.message}</p>
8574
<input
8675
className={styles.input}
8776
type='email'
8877
placeholder='email'
89-
{...register('Email', {
90-
required: true,
91-
pattern: /^\S+@\S+$/i,
92-
})}
78+
{...register('email')}
9379
/>
94-
<p className={styles['error-msg']}>
95-
{errors.Email?.type === 'required' && 'Email is required'}
96-
</p>
80+
<p className={styles['error-msg']}>{errors.email?.message}</p>
9781
<input
9882
className={styles.input}
9983
type='text'
10084
placeholder='subject'
101-
{...register('Subject', {
102-
required: true,
103-
minLength: 2,
104-
pattern: /[^\s-]/i,
105-
})}
85+
{...register('subject')}
10686
/>
107-
<p className={styles['error-msg']}>
108-
{errors.Subject?.type === 'required'
109-
? 'Subject is required'
110-
: errors.Subject?.type === 'pattern'
111-
? 'No whitespace'
112-
: errors.Subject?.type === 'minLength'
113-
? 'Must be more than 1 character'
114-
: undefined}
115-
</p>
87+
<p className={styles['error-msg']}>{errors.subject?.message}</p>
11688
<textarea
11789
className={styles.textarea}
118-
{...register('Message', {
119-
required: true,
120-
minLength: 2,
121-
pattern: /[^\s-]/i,
122-
})}
90+
{...register('message')}
12391
placeholder='Write your message here'
12492
/>
125-
<p className={styles['error-msg']}>
126-
{errors.Message?.type === 'required'
127-
? 'Message is required'
128-
: errors.Message?.type === 'pattern'
129-
? 'No whitespace'
130-
: errors.Message?.type === 'minLength'
131-
? 'Must be more than 1 character'
132-
: undefined}
133-
</p>
93+
<p className={styles['error-msg']}>{errors.message?.message}</p>
13494
<label className={styles['subscribe-wrapper']}>
13595
<input
13696
className={styles['subscribe-input']}
13797
type='checkbox'
13898
placeholder='Subscribe to our DevNews!'
139-
{...register('Subscribe', {})}
99+
{...register('subscribe')}
140100
/>
141101
Subscribe to our DevNews!
142102
</label>

components/NewsletterSubscribe/NewsletterForm/index.js

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { decode } from 'html-entities';
44
import { NewsLetterSubmitButton } from '@/components/buttons/SubmitButton';
55
import styles from './NewsletterForm.module.scss';
66
import Container from '@/components/containers/Container';
7+
import { newsletterSchema } from '@/utils/schemas/newsletter';
78

89
const NewsletterForm = ({ getReCaptchaToken }) => {
910
const [error, setError] = useState(null);
@@ -61,13 +62,9 @@ const NewsletterForm = ({ getReCaptchaToken }) => {
6162

6263
setError(null);
6364

64-
if (!name) {
65-
setError('Please enter a name');
66-
return null;
67-
}
68-
69-
if (!email) {
70-
setError('Please enter a valid email address');
65+
const result = newsletterSchema.safeParse({ name, email });
66+
if (!result.success) {
67+
setError(result.error.issues[0].message);
7168
return null;
7269
}
7370

@@ -152,19 +149,14 @@ const NewsletterForm = ({ getReCaptchaToken }) => {
152149
{status === 'sending' && (
153150
<div className={styles.formSending}>Sending...</div>
154151
)}
155-
{status === 'error' || error ? (
156-
<div
157-
className={styles.formError}
158-
dangerouslySetInnerHTML={{
159-
__html: error || getMessage(message),
160-
}}
161-
/>
162-
) : null}
163-
{status === 'success' && status !== 'error' && !error && (
164-
<div
165-
className={styles.formSuccess}
166-
dangerouslySetInnerHTML={{ __html: decode(message) }}
167-
/>
152+
{(status === 'error' || error) && (
153+
<div className={styles.formError}>
154+
{error || getMessage(message)}
155+
</div>
156+
)}
157+
158+
{status === 'success' && !error && (
159+
<div className={styles.formSuccess}>{decode(message)}</div>
168160
)}
169161
</div>
170162
</div>

jsconfig.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
{
22
"compilerOptions": {
3-
"baseUrl": ".",
43
"paths": {
5-
"@/components/*": ["components/*"],
6-
"@/styles/*": ["styles/*"],
7-
"@/hooks/*": ["hooks/*"],
8-
"@/utils/*": ["utils/*"]
4+
"@/components/*": ["./components/*"],
5+
"@/styles/*": ["./styles/*"],
6+
"@/hooks/*": ["./hooks/*"],
7+
"@/utils/*": ["./utils/*"]
98
},
109
"types": []
1110
}

next.config.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
const withPWA = require('next-pwa')({
2-
dest: 'public',
3-
register: true,
4-
skipWaiting: true,
5-
disable: process.env.NODE_ENV === 'development',
6-
});
1+
const withPWA = require('next-pwa');
72

83
module.exports = withPWA({
4+
pwa: {
5+
dest: 'public',
6+
register: true,
7+
skipWaiting: true,
8+
disable: process.env.NODE_ENV === 'development',
9+
},
910
compiler: {
1011
styledComponents: {
1112
ssr: true,

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,19 @@
1919
]
2020
},
2121
"dependencies": {
22+
"@hookform/resolvers": "^5.2.2",
2223
"@sendgrid/mail": "^8.1.5",
2324
"html-entities": "^2.3.2",
24-
"next": "15.3.8",
25-
"next-pwa": "^5.6.0",
25+
"next": "^15.5.14",
26+
"next-pwa": "^2.0.2",
2627
"node-mailjet": "^6.0.9",
2728
"react": "19.1.0",
2829
"react-dom": "19.1.0",
2930
"react-google-recaptcha": "^3.1.0",
3031
"react-hook-form": "^7.35.0",
3132
"sass": "^1.35.1",
32-
"swiper": "^11.2.6"
33+
"swiper": "^12.1.3",
34+
"zod": "^4.3.6"
3335
},
3436
"devDependencies": {
3537
"husky": "^9.1.7",

pages/api/sendEmail.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Sends email to hello@webdevpath.co when user submit the form in "Contact Us" page
22

33
import { Client } from 'node-mailjet';
4+
import { encode } from 'html-entities';
45

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

1920
try {
21+
const safeName = encode(name);
22+
const safeEmail = encode(email);
23+
const safeSubject = encode(subject);
24+
const safeMessage = encode(message);
25+
2026
const data = {
2127
Messages: [
2228
{
@@ -29,12 +35,12 @@ export default async (email, name, subject, message, subscribe) => {
2935
Email: receiverEmail,
3036
},
3137
],
32-
Subject: `New message from ${name} via webdevpath.co 'Contact Us' Form`,
38+
Subject: `New message from ${safeName} via webdevpath.co 'Contact Us' Form`,
3339
HTMLPart: `
34-
<b>Name:</b> ${name} <br/>
35-
<b>Email:</b> <a href='mailto:${email}'>${email}</a><br/><br/>
36-
<u><b>Subject:</b> ${subject}</u><br/>
37-
<b>Message:</b> ${message} <br/>
40+
<b>Name:</b> ${safeName} <br/>
41+
<b>Email:</b> <a href='mailto:${safeEmail}'>${safeEmail}</a><br/><br/>
42+
<u><b>Subject:</b> ${safeSubject}</u><br/>
43+
<b>Message:</b> ${safeMessage} <br/>
3844
<b>Subscribe?:</b> ${subscribe ? 'Yes' : 'No'}
3945
`,
4046
},

pages/api/validateReCaptcha.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import sendEmail from './sendEmail.js';
22
import { subscribeToMailchimp } from '../../lib/mailchimp';
3+
import { contactSchema } from '../../utils/schemas/contact';
4+
import { newsletterSchema } from '../../utils/schemas/newsletter';
35

46
export default async function handler(req, res) {
57
const { method } = req;
@@ -12,13 +14,25 @@ export default async function handler(req, res) {
1214
const { name, email, subject, message, subscribe, gReCaptchaToken } =
1315
req.body;
1416

15-
// If email or captcha are missing return an error
16-
if (!email || !name || !gReCaptchaToken) {
17+
if (!gReCaptchaToken) {
1718
return res.status(422).json({
1819
message: 'Unprocessable request, please provide the required fields',
1920
});
2021
}
2122

23+
const isContactForm = subject || message;
24+
const schema = isContactForm ? contactSchema : newsletterSchema;
25+
const parseData = isContactForm
26+
? { name, email, subject, message, subscribe }
27+
: { name, email };
28+
29+
const validationResult = schema.safeParse(parseData);
30+
if (!validationResult.success) {
31+
return res.status(422).json({
32+
message: validationResult.error.issues[0].message,
33+
});
34+
}
35+
2236
try {
2337
// Ping the google recaptcha verify API to verify the captcha code you received
2438
const response = await fetch(

utils/schemas/contact.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { z } from 'zod';
2+
import { newsletterSchema } from './newsletter';
3+
4+
export const contactSchema = newsletterSchema.extend({
5+
subject: z.string().min(2, 'Subject must be at least 2 characters'),
6+
message: z
7+
.string()
8+
.min(2, 'Message must be at least 2 characters')
9+
.max(5000, 'Message must be 5000 characters or less'),
10+
subscribe: z.boolean().optional(),
11+
});

utils/schemas/newsletter.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { z } from 'zod';
2+
3+
export const newsletterSchema = z.object({
4+
name: z
5+
.string()
6+
.min(2, 'Name must be at least 2 characters')
7+
.max(80, 'Name must be 80 characters or less'),
8+
email: z.string().email('Please enter a valid email address'),
9+
});

0 commit comments

Comments
 (0)