Compare commits

..

5 Commits

Author SHA1 Message Date
Felipe Cardoso
a3e557d022 Update E2E test for security headers to include worker-src validation 2025-12-26 19:00:18 +01:00
Felipe Cardoso
4e357db25d Update E2E test for security headers to include worker-src validation 2025-12-26 19:00:11 +01:00
Felipe Cardoso
568aad3673 Add E2E tests for security headers
- Implemented tests to verify OWASP-compliant security headers, including X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and Content-Security-Policy.
- Ensured deprecated headers like X-XSS-Protection are not set.
- Validated security headers across multiple routes.
- Updated Playwright configuration to include the new test suite.
2025-12-10 14:53:40 +01:00
Felipe Cardoso
ddcf926158 Add OWASP-compliant security headers to Next.js configuration
- Implemented security headers following OWASP 2025 recommendations, including X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and Content-Security-Policy.
- Applied headers globally across all routes for enhanced security.
2025-12-10 13:55:15 +01:00
Felipe Cardoso
865eeece58 Update dependencies in package-lock.json
- Upgraded multiple packages including `@next/*`, `next`, `js-yaml`, `glob`, and `mdast-util-to-hast` to ensure compatibility and enhance performance.
- Addressed potential security and functionality improvements with newer versions.
2025-12-10 11:19:59 +01:00
4 changed files with 186 additions and 54 deletions

View File

@@ -0,0 +1,62 @@
/**
* E2E Tests for Security Headers
* Verifies that security headers are properly configured per OWASP 2025 recommendations
*
* References:
* - https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html
* - https://nextjs.org/docs/app/api-reference/config/next-config-js/headers
*/
import { test, expect } from '@playwright/test';
test.describe('Security Headers', () => {
test('should include all required security headers', async ({ request }) => {
const response = await request.get('/');
const headers = response.headers();
// X-Frame-Options: Prevents clickjacking attacks
expect(headers['x-frame-options']).toBe('DENY');
// X-Content-Type-Options: Prevents MIME type sniffing
expect(headers['x-content-type-options']).toBe('nosniff');
// Referrer-Policy: Controls referrer information leakage
expect(headers['referrer-policy']).toBe('strict-origin-when-cross-origin');
// Permissions-Policy: Restricts browser features
expect(headers['permissions-policy']).toContain('camera=()');
expect(headers['permissions-policy']).toContain('microphone=()');
expect(headers['permissions-policy']).toContain('geolocation=()');
// Content-Security-Policy: Mitigates XSS and injection attacks
const csp = headers['content-security-policy'];
expect(csp).toBeDefined();
expect(csp).toContain("default-src 'self'");
expect(csp).toContain("frame-ancestors 'none'");
expect(csp).toContain("object-src 'none'");
expect(csp).toContain("worker-src 'self' blob:"); // Required for MSW
});
test('should NOT include deprecated security headers', async ({ request }) => {
const response = await request.get('/');
const headers = response.headers();
// X-XSS-Protection is deprecated and should not be set
// (Modern browsers have removed support, can cause security issues)
expect(headers['x-xss-protection']).toBeUndefined();
});
test('security headers should be present on all routes', async ({ request }) => {
const routes = ['/', '/en', '/en/login', '/en/register'];
for (const route of routes) {
const response = await request.get(route);
const headers = response.headers();
expect(headers['x-frame-options'], `Missing X-Frame-Options on ${route}`).toBe('DENY');
expect(headers['x-content-type-options'], `Missing X-Content-Type-Options on ${route}`).toBe(
'nosniff'
);
}
});
});

View File

@@ -4,8 +4,76 @@ import createNextIntlPlugin from 'next-intl/plugin';
// Initialize next-intl plugin with i18n request config path
const withNextIntl = createNextIntlPlugin('./src/lib/i18n/request.ts');
/**
* Security Headers Configuration (OWASP 2025 recommendations)
*
* References:
* - https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html
* - https://nextjs.org/docs/app/api-reference/config/next-config-js/headers
*
* Note: X-XSS-Protection is intentionally omitted (deprecated, removed from browsers)
* Note: Strict CSP requires dynamic rendering with nonces - not implemented here
*/
const securityHeaders = [
{
// Prevents clickjacking by denying iframe embedding
// Also handled by CSP frame-ancestors, but X-Frame-Options provides legacy browser support
key: 'X-Frame-Options',
value: 'DENY',
},
{
// Prevents MIME type sniffing attacks
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
// Controls how much referrer information is sent with requests
// 'strict-origin-when-cross-origin' is the recommended balance of privacy and functionality
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
// Disables browser features that aren't needed
// Add features back as needed: camera=self, microphone=self, etc.
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()',
},
{
// Basic CSP that works with inline scripts/styles (required for theme init and Tailwind)
// For strict CSP with nonces, use proxy.ts with dynamic rendering
// frame-ancestors 'none' duplicates X-Frame-Options for modern browsers
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // unsafe-eval needed for MSW in dev
"style-src 'self' 'unsafe-inline'", // Required for Tailwind and styled components
"img-src 'self' blob: data: https:", // Allow images from HTTPS sources
"font-src 'self'",
"connect-src 'self' http://localhost:* ws://localhost:*", // API + HMR websocket
"worker-src 'self' blob:", // Required for MSW service worker in demo mode
"child-src 'self' blob:", // For service worker registration
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
].join('; '),
},
];
const nextConfig: NextConfig = {
output: 'standalone',
// Security headers applied to all routes
async headers() {
return [
{
// Apply to all routes
source: '/(.*)',
headers: securityHeaders,
},
];
},
// Ensure we can connect to the backend in Docker
async rewrites() {
return [
@@ -15,6 +83,7 @@ const nextConfig: NextConfig = {
},
];
},
// Production optimizations
reactStrictMode: true,
// Note: SWC minification is default in Next.js 16

View File

@@ -2286,9 +2286,9 @@
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2917,9 +2917,9 @@
}
},
"node_modules/@next/env": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.3.tgz",
"integrity": "sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ==",
"version": "16.0.8",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.8.tgz",
"integrity": "sha512-xP4WrQZuj9MdmLJy3eWFHepo+R3vznsMSS8Dy3wdA7FKpjCiesQ6DxZvdGziQisj0tEtCgBKJzjcAc4yZOgLEQ==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -2933,9 +2933,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.3.tgz",
"integrity": "sha512-MOnbd92+OByu0p6QBAzq1ahVWzF6nyfiH07dQDez4/Nku7G249NjxDVyEfVhz8WkLiOEU+KFVnqtgcsfP2nLXg==",
"version": "16.0.8",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.8.tgz",
"integrity": "sha512-yjVMvTQN21ZHOclQnhSFbjBTEizle+1uo4NV6L4rtS9WO3nfjaeJYw+H91G+nEf3Ef43TaEZvY5mPWfB/De7tA==",
"cpu": [
"arm64"
],
@@ -2949,9 +2949,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.3.tgz",
"integrity": "sha512-i70C4O1VmbTivYdRlk+5lj9xRc2BlK3oUikt3yJeHT1unL4LsNtN7UiOhVanFdc7vDAgZn1tV/9mQwMkWOJvHg==",
"version": "16.0.8",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.8.tgz",
"integrity": "sha512-+zu2N3QQ0ZOb6RyqQKfcu/pn0UPGmg+mUDqpAAEviAcEVEYgDckemOpiMRsBP3IsEKpcoKuNzekDcPczEeEIzA==",
"cpu": [
"x64"
],
@@ -2965,9 +2965,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.3.tgz",
"integrity": "sha512-O88gCZ95sScwD00mn/AtalyCoykhhlokxH/wi1huFK+rmiP5LAYVs/i2ruk7xST6SuXN4NI5y4Xf5vepb2jf6A==",
"version": "16.0.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.8.tgz",
"integrity": "sha512-LConttk+BeD0e6RG0jGEP9GfvdaBVMYsLJ5aDDweKiJVVCu6sGvo+Ohz9nQhvj7EQDVVRJMCGhl19DmJwGr6bQ==",
"cpu": [
"arm64"
],
@@ -2981,9 +2981,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.3.tgz",
"integrity": "sha512-CEErFt78S/zYXzFIiv18iQCbRbLgBluS8z1TNDQoyPi8/Jr5qhR3e8XHAIxVxPBjDbEMITprqELVc5KTfFj0gg==",
"version": "16.0.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.8.tgz",
"integrity": "sha512-JaXFAlqn8fJV+GhhA9lpg6da/NCN/v9ub98n3HoayoUSPOVdoxEEt86iT58jXqQCs/R3dv5ZnxGkW8aF4obMrQ==",
"cpu": [
"arm64"
],
@@ -2997,9 +2997,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.3.tgz",
"integrity": "sha512-Tc3i+nwt6mQ+Dwzcri/WNDj56iWdycGVh5YwwklleClzPzz7UpfaMw1ci7bLl6GRYMXhWDBfe707EXNjKtiswQ==",
"version": "16.0.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.8.tgz",
"integrity": "sha512-O7M9it6HyNhsJp3HNAsJoHk5BUsfj7hRshfptpGcVsPZ1u0KQ/oVy8oxF7tlwxA5tR43VUP0yRmAGm1us514ng==",
"cpu": [
"x64"
],
@@ -3013,9 +3013,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.3.tgz",
"integrity": "sha512-zTh03Z/5PBBPdTurgEtr6nY0vI9KR9Ifp/jZCcHlODzwVOEKcKRBtQIGrkc7izFgOMuXDEJBmirwpGqdM/ZixA==",
"version": "16.0.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.8.tgz",
"integrity": "sha512-8+KClEC/GLI2dLYcrWwHu5JyC5cZYCFnccVIvmxpo6K+XQt4qzqM5L4coofNDZYkct/VCCyJWGbZZDsg6w6LFA==",
"cpu": [
"x64"
],
@@ -3029,9 +3029,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.3.tgz",
"integrity": "sha512-Jc1EHxtZovcJcg5zU43X3tuqzl/sS+CmLgjRP28ZT4vk869Ncm2NoF8qSTaL99gh6uOzgM99Shct06pSO6kA6g==",
"version": "16.0.8",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.8.tgz",
"integrity": "sha512-rpQ/PgTEgH68SiXmhu/cJ2hk9aZ6YgFvspzQWe2I9HufY6g7V02DXRr/xrVqOaKm2lenBFPNQ+KAaeveywqV+A==",
"cpu": [
"arm64"
],
@@ -3045,9 +3045,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.3.tgz",
"integrity": "sha512-N7EJ6zbxgIYpI/sWNzpVKRMbfEGgsWuOIvzkML7wxAAZhPk1Msxuo/JDu1PKjWGrAoOLaZcIX5s+/pF5LIbBBg==",
"version": "16.0.8",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.8.tgz",
"integrity": "sha512-jWpWjWcMQu2iZz4pEK2IktcfR+OA9+cCG8zenyLpcW8rN4rzjfOzH4yj/b1FiEAZHKS+5Vq8+bZyHi+2yqHbFA==",
"cpu": [
"x64"
],
@@ -10131,9 +10131,9 @@
"license": "ISC"
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -10281,9 +10281,9 @@
}
},
"node_modules/gray-matter/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
@@ -12513,9 +12513,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -13514,9 +13514,9 @@
}
},
"node_modules/mdast-util-to-hast": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"version": "13.2.1",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
@@ -14460,12 +14460,12 @@
}
},
"node_modules/next": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/next/-/next-16.0.3.tgz",
"integrity": "sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w==",
"version": "16.0.8",
"resolved": "https://registry.npmjs.org/next/-/next-16.0.8.tgz",
"integrity": "sha512-LmcZzG04JuzNXi48s5P+TnJBsTGPJunViNKV/iE4uM6kstjTQsQhvsAv+xF6MJxU2Pr26tl15eVbp0jQnsv6/g==",
"license": "MIT",
"dependencies": {
"@next/env": "16.0.3",
"@next/env": "16.0.8",
"@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@@ -14478,14 +14478,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.0.3",
"@next/swc-darwin-x64": "16.0.3",
"@next/swc-linux-arm64-gnu": "16.0.3",
"@next/swc-linux-arm64-musl": "16.0.3",
"@next/swc-linux-x64-gnu": "16.0.3",
"@next/swc-linux-x64-musl": "16.0.3",
"@next/swc-win32-arm64-msvc": "16.0.3",
"@next/swc-win32-x64-msvc": "16.0.3",
"@next/swc-darwin-arm64": "16.0.8",
"@next/swc-darwin-x64": "16.0.8",
"@next/swc-linux-arm64-gnu": "16.0.8",
"@next/swc-linux-arm64-musl": "16.0.8",
"@next/swc-linux-x64-gnu": "16.0.8",
"@next/swc-linux-x64-musl": "16.0.8",
"@next/swc-win32-arm64-msvc": "16.0.8",
"@next/swc-win32-x64-msvc": "16.0.8",
"sharp": "^0.34.4"
},
"peerDependencies": {

View File

@@ -113,6 +113,7 @@ export default defineConfig({
/auth-flows\.spec\.ts/,
/auth-oauth\.spec\.ts/,
/theme-toggle\.spec\.ts/,
/security-headers\.spec\.ts/,
],
use: { ...devices['Desktop Chrome'] },
},