Skip to content

Commit 405f179

Browse files
authored
Update NextJS version (#533)
Update NextJS version to `15.5.3` and `@sentry/nextjs` to `10.11.0`
1 parent 6c5d7ff commit 405f179

File tree

5 files changed

+744
-518
lines changed

5 files changed

+744
-518
lines changed

cypress/e2e/security/image.cy.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
describe('Image Security Tests', () => {
2+
beforeEach(() => {
3+
cy.visit('http://localhost:3000');
4+
});
5+
6+
afterEach(() => {
7+
cy.a11yCheck();
8+
});
9+
10+
it('should only allow configured remote image domains', () => {
11+
// Based on your next.config.js, only alexjsully.me should be allowed
12+
const disallowedDomain = 'https://random-external-site.com/image.jpg';
13+
14+
cy.request({
15+
url: `/_next/image?url=${encodeURIComponent(disallowedDomain)}&w=640&q=75`,
16+
failOnStatusCode: false,
17+
}).then((response) => {
18+
// Should return error status, not serve the image
19+
expect(response.status).to.not.equal(200);
20+
});
21+
});
22+
23+
it('should prevent file download attacks through image optimization', () => {
24+
// Test that image optimization doesn't allow arbitrary file downloads
25+
const maliciousParams = ['../../../../etc/passwd', 'file:///etc/hosts'];
26+
27+
maliciousParams.forEach((param) => {
28+
cy.request({
29+
url: `/_next/image?url=${encodeURIComponent(param)}&w=640&q=75`,
30+
failOnStatusCode: false,
31+
}).then((response) => {
32+
// Should return error status, not serve files
33+
expect(response.status).to.not.equal(200);
34+
const contentType = response.headers['content-type'] || '';
35+
expect(contentType).to.not.include('text/plain');
36+
expect(contentType).to.not.include('application/octet-stream');
37+
});
38+
});
39+
});
40+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
describe('Middleware Security Tests', () => {
2+
beforeEach(() => {
3+
cy.visit('http://localhost:3000');
4+
});
5+
6+
afterEach(() => {
7+
cy.a11yCheck();
8+
});
9+
10+
it('should prevent SSRF through middleware headers', () => {
11+
// Test that sensitive headers are not reflected back
12+
const sensitiveHeaders = {
13+
'X-Forwarded-Host': 'evil.com',
14+
'X-Forwarded-Proto': 'http',
15+
'X-Real-IP': '127.0.0.1',
16+
'X-Forwarded-For': '192.168.1.1, evil.com',
17+
Host: 'attacker.site',
18+
};
19+
20+
Object.entries(sensitiveHeaders).forEach(([headerName, headerValue]) => {
21+
cy.request({
22+
url: 'http://localhost:3000',
23+
headers: {
24+
[headerName]: headerValue,
25+
},
26+
failOnStatusCode: false,
27+
}).then((response) => {
28+
// Check that sensitive headers are not reflected in response
29+
const responseHeaders = Object.keys(response.headers).map((key) => key.toLowerCase());
30+
const responseHeaderValues = Object.values(response.headers).join(' ');
31+
32+
// Ensure the malicious header value is not reflected back
33+
expect(responseHeaderValues).to.not.include(headerValue);
34+
35+
// Check that response doesn't contain redirect to external domain
36+
if (response.status >= 300 && response.status < 400) {
37+
const location = response.headers.location;
38+
if (location) {
39+
expect(location).to.not.include('evil.com');
40+
expect(location).to.not.include('attacker.site');
41+
}
42+
}
43+
});
44+
});
45+
});
46+
47+
it('should validate redirect destinations in middleware', () => {
48+
// Test that redirects don't allow SSRF
49+
const maliciousRedirects = [
50+
'http://evil.com',
51+
'https://attacker.site/steal-data',
52+
'ftp://internal-server.local',
53+
'file:///etc/passwd',
54+
'gopher://localhost:25',
55+
];
56+
57+
maliciousRedirects.forEach((redirectUrl) => {
58+
cy.request({
59+
url: `http://localhost:3000?redirect=${encodeURIComponent(redirectUrl)}`,
60+
followRedirect: false,
61+
failOnStatusCode: false,
62+
}).then((response) => {
63+
if (response.status >= 300 && response.status < 400) {
64+
const location = response.headers.location;
65+
if (location) {
66+
// Should not redirect to external or malicious URLs
67+
expect(location).to.not.include('evil.com');
68+
expect(location).to.not.include('attacker.site');
69+
expect(location).to.not.match(/^(ftp|file|gopher):/);
70+
71+
// Should only redirect to same origin or relative paths
72+
if (typeof location === 'string' && location.startsWith('http')) {
73+
expect(location).to.match(/^https?:\/\/localhost(:\d+)?/);
74+
}
75+
}
76+
}
77+
});
78+
});
79+
});
80+
81+
it('should sanitize request headers in middleware processing', () => {
82+
// Test that middleware doesn't process dangerous header combinations
83+
cy.request({
84+
url: 'http://localhost:3000',
85+
headers: {
86+
'X-Forwarded-Host': 'evil.com',
87+
'X-Forwarded-Proto': 'javascript',
88+
'X-Original-URL': '/admin/secret',
89+
'X-Rewrite-URL': '/../../etc/passwd',
90+
},
91+
failOnStatusCode: false,
92+
}).then((response) => {
93+
// Verify response doesn't expose internal paths or dangerous content
94+
expect(response.body).to.not.include('/etc/passwd');
95+
expect(response.body).to.not.include('/admin/secret');
96+
expect(response.status).to.not.equal(500); // Should handle gracefully
97+
});
98+
});
99+
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
describe('Security Tests', () => {
1+
describe('General Security Tests', () => {
22
beforeEach(() => {
33
cy.visit('http://localhost:3000');
44
});

0 commit comments

Comments
 (0)