diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 94eb3a2bd0..c21eb614e8 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -3,9 +3,41 @@ import type { NextConfig } from 'next' const forceStandalone = process.env.FORCE_STANDALONE === 'yes' const isLocal = process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' +const contentSecurityPolicy = + "default-src 'self'; script-src 'self' 'unsafe-inline' https://owasp-nest.s3.amazonaws.com https://owasp-nest-production.s3.amazonaws.com https://www.googletagmanager.com https://*.i.posthog.com https://*.tile.openstreetmap.org; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://owasp-nest.s3.amazonaws.com https://owasp-nest-production.s3.amazonaws.com; img-src 'self' data: https://authjs.dev https://avatars.githubusercontent.com https://*.tile.openstreetmap.org https://owasp.org https://owasp-nest.s3.amazonaws.com https://owasp-nest-production.s3.amazonaws.com https://raw.githubusercontent.com; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self' https://github-contributions-api.jogruber.de https://*.google-analytics.com https://*.i.posthog.com https://*.sentry.io https://*.tile.openstreetmap.org; object-src 'none'; frame-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" +const contentSecurityPolicyLocal = contentSecurityPolicy.replace( + "script-src 'self' 'unsafe-inline'", + "script-src 'self' 'unsafe-inline' 'unsafe-eval'" +) +const securityHeaders = [ + { + key: 'Content-Security-Policy', + value: isLocal ? contentSecurityPolicyLocal : contentSecurityPolicy, + }, + { + key: 'Permissions-Policy', + value: 'camera=(), fullscreen=(self), geolocation=(self), microphone=()', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'X-Frame-Options', + value: 'DENY', + }, +] const nextConfig: NextConfig = { devIndicators: false, + async headers() { + return [ + { + headers: securityHeaders, + source: '/(.*)', + }, + ] + }, images: { // This is a list of remote patterns that Next.js will use to determine // if an image is allowed to be loaded from a remote source. diff --git a/infrastructure/modules/alb/main.tf b/infrastructure/modules/alb/main.tf index 75dc237fbb..5c7929d051 100644 --- a/infrastructure/modules/alb/main.tf +++ b/infrastructure/modules/alb/main.tf @@ -32,7 +32,8 @@ locals { "/status", "/status/*", ] - backend_path_chunks = chunklist(local.backend_paths, 5) + backend_path_chunks = chunklist(local.backend_paths, 5) + content_security_policy = "default-src 'self'; script-src 'self' 'unsafe-inline' https://owasp-nest.s3.amazonaws.com https://owasp-nest-production.s3.amazonaws.com https://www.googletagmanager.com https://*.i.posthog.com https://*.tile.openstreetmap.org; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://owasp-nest.s3.amazonaws.com https://owasp-nest-production.s3.amazonaws.com; img-src 'self' data: https://authjs.dev https://avatars.githubusercontent.com https://*.tile.openstreetmap.org https://owasp.org https://owasp-nest.s3.amazonaws.com https://owasp-nest-production.s3.amazonaws.com https://raw.githubusercontent.com; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self' https://github-contributions-api.jogruber.de https://*.google-analytics.com https://*.i.posthog.com https://*.sentry.io https://*.tile.openstreetmap.org; object-src 'none'; frame-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" } data "aws_elb_service_account" "main" {} @@ -127,12 +128,16 @@ resource "aws_lb_listener" "http_redirect" { } resource "aws_lb_listener" "https" { - certificate_arn = aws_acm_certificate_validation.main.certificate_arn - load_balancer_arn = aws_lb.main.arn - port = 443 - protocol = "HTTPS" - ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" - tags = var.common_tags + certificate_arn = aws_acm_certificate_validation.main.certificate_arn + load_balancer_arn = aws_lb.main.arn + port = 443 + protocol = "HTTPS" + routing_http_response_content_security_policy_header_value = local.content_security_policy + routing_http_response_strict_transport_security_header_value = "max-age=31536000; includeSubDomains; preload" + routing_http_response_x_content_type_options_header_value = "nosniff" + routing_http_response_x_frame_options_header_value = "DENY" + ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" + tags = var.common_tags default_action { target_group_arn = aws_lb_target_group.frontend.arn diff --git a/infrastructure/modules/alb/tests/alb.tftest.hcl b/infrastructure/modules/alb/tests/alb.tftest.hcl index 050c6dddaa..0d24794605 100644 --- a/infrastructure/modules/alb/tests/alb.tftest.hcl +++ b/infrastructure/modules/alb/tests/alb.tftest.hcl @@ -215,6 +215,38 @@ run "test_https_listener_ssl_policy" { } } +run "test_https_listener_sets_hsts_header" { + command = plan + assert { + condition = aws_lb_listener.https.routing_http_response_strict_transport_security_header_value == "max-age=31536000; includeSubDomains; preload" + error_message = "HTTPS listener must set HSTS header value." + } +} + +run "test_https_listener_sets_x_frame_options_header" { + command = plan + assert { + condition = aws_lb_listener.https.routing_http_response_x_frame_options_header_value == "DENY" + error_message = "HTTPS listener must set X-Frame-Options header value." + } +} + +run "test_https_listener_sets_x_content_type_options_header" { + command = plan + assert { + condition = aws_lb_listener.https.routing_http_response_x_content_type_options_header_value == "nosniff" + error_message = "HTTPS listener must set X-Content-Type-Options header value." + } +} + +run "test_https_listener_sets_csp_header" { + command = plan + assert { + condition = aws_lb_listener.https.routing_http_response_content_security_policy_header_value == "default-src 'self'; script-src 'self' 'unsafe-inline' https://owasp-nest.s3.amazonaws.com https://owasp-nest-production.s3.amazonaws.com https://www.googletagmanager.com https://*.i.posthog.com https://*.tile.openstreetmap.org; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://owasp-nest.s3.amazonaws.com https://owasp-nest-production.s3.amazonaws.com; img-src 'self' data: https://authjs.dev https://avatars.githubusercontent.com https://*.tile.openstreetmap.org https://owasp.org https://owasp-nest.s3.amazonaws.com https://owasp-nest-production.s3.amazonaws.com https://raw.githubusercontent.com; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self' https://github-contributions-api.jogruber.de https://*.google-analytics.com https://*.i.posthog.com https://*.sentry.io https://*.tile.openstreetmap.org; object-src 'none'; frame-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" + error_message = "HTTPS listener must set Content-Security-Policy header value." + } +} + run "test_backend_listener_rules_not_created_when_null" { command = plan variables {