From cbe92da7bc2971306535345b1ebf63190e1d2805 Mon Sep 17 00:00:00 2001 From: Gandalf Date: Wed, 31 Dec 2025 00:09:00 +0100 Subject: [PATCH 1/2] feat: Add security response headers Add X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy headers to all responses from blog and wallet apps. These headers provide defense-in-depth protection: - X-Content-Type-Options: nosniff - prevents MIME-sniffing attacks - X-Frame-Options: SAMEORIGIN - prevents clickjacking - Referrer-Policy: strict-origin-when-cross-origin - controls referrer leakage - Permissions-Policy: disables unused browser features CSP is intentionally not included yet - it will be added separately after proper testing with Report-Only mode. Also adds documentation for nginx configuration to avoid header conflicts and duplication between app and reverse proxy. --- apps/blog/next.config.js | 28 +++++++- apps/wallet/next.config.js | 27 ++++++++ docs/security-headers.md | 136 +++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 docs/security-headers.md diff --git a/apps/blog/next.config.js b/apps/blog/next.config.js index 8cfc7edbc..a35ec3620 100644 --- a/apps/blog/next.config.js +++ b/apps/blog/next.config.js @@ -9,6 +9,28 @@ const withPWA = require('next-pwa')({ // Support serving from subdirectory like /blog const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''; +// Security headers applied to all responses +// Note: CSP is intentionally not included here - it will be added separately +// after proper testing with Report-Only mode. HSTS should be set at nginx level. +const securityHeaders = [ + { + key: 'X-Content-Type-Options', + value: 'nosniff' + }, + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN' + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin' + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=()' + } +]; + /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, @@ -26,6 +48,11 @@ const nextConfig = { /// According to notes: https://nextjs.org/docs/app/guides/progressive-web-apps#8-securing-your-application async headers() { return [ + // Security headers for all routes + { + source: '/:path*', + headers: securityHeaders + }, { source: '/sw.js', headers: [ @@ -51,7 +78,6 @@ const nextConfig = { value: 'no-cache, no-store, must-revalidate, max-age=0' } ] - } ]; }, diff --git a/apps/wallet/next.config.js b/apps/wallet/next.config.js index 97d21d0d7..2c556c54d 100644 --- a/apps/wallet/next.config.js +++ b/apps/wallet/next.config.js @@ -9,6 +9,28 @@ const withPWA = require('next-pwa')({ // Get basePath from environment variable at build time const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''; +// Security headers applied to all responses +// Note: CSP is intentionally not included here - it will be added separately +// after proper testing with Report-Only mode. HSTS should be set at nginx level. +const securityHeaders = [ + { + key: 'X-Content-Type-Options', + value: 'nosniff' + }, + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN' + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin' + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=()' + } +]; + /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, @@ -41,6 +63,11 @@ const nextConfig = { /// According to notes: https://nextjs.org/docs/app/guides/progressive-web-apps#8-securing-your-application async headers() { return [ + // Security headers for all routes + { + source: '/:path*', + headers: securityHeaders + }, { source: '/__ENV.js', headers: [ diff --git a/docs/security-headers.md b/docs/security-headers.md new file mode 100644 index 000000000..d52416f79 --- /dev/null +++ b/docs/security-headers.md @@ -0,0 +1,136 @@ +# Security Headers Configuration + +This document describes the security headers implementation for Denser and provides guidance for nginx reverse proxy configuration. + +## Overview + +Security headers are set at the **application level** (Next.js) to ensure they are version-controlled with the code and work consistently across all deployment environments. + +## Headers Set by the Application + +The following headers are configured in `apps/blog/next.config.js` and `apps/wallet/next.config.js`: + +| Header | Value | Purpose | +|--------|-------|---------| +| X-Content-Type-Options | `nosniff` | Prevents MIME-sniffing attacks | +| X-Frame-Options | `SAMEORIGIN` | Prevents clickjacking by restricting iframe embedding | +| Referrer-Policy | `strict-origin-when-cross-origin` | Controls referrer information sent to other sites | +| Permissions-Policy | `camera=(), microphone=(), geolocation=()` | Disables unused browser features | + +### Future Headers (Planned) + +| Header | Status | Notes | +|--------|--------|-------| +| Content-Security-Policy | Planned | Will be added after testing with Report-Only mode | +| Strict-Transport-Security | Set by nginx | Infrastructure concern, see below | + +## Nginx Configuration Guidelines + +To avoid header conflicts and duplication, follow these guidelines: + +### Principle: Single Source of Truth + +Each header should be set in **one place only** - either the application or nginx, not both. + +### Recommended Nginx Configuration + +```nginx +server { + listen 443 ssl http2; + server_name example.com; + + # SSL configuration... + + location / { + proxy_pass http://nextjs_upstream; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + + # HSTS - Set at nginx level (infrastructure concern) + # Only enable after confirming HTTPS works correctly + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # DO NOT set these headers in nginx - they are set by the application: + # - X-Content-Type-Options + # - X-Frame-Options + # - Referrer-Policy + # - Permissions-Policy + # - Content-Security-Policy (when implemented) + } + + # Static assets - can have longer cache and same security headers + location /_next/static/ { + proxy_pass http://nextjs_upstream; + add_header Cache-Control "public, max-age=31536000, immutable"; + # Security headers are inherited from the app response + } +} +``` + +### Why HSTS Should Be Set at Nginx Level + +Strict-Transport-Security (HSTS) is an infrastructure concern because: +1. It affects the entire domain, not just the application +2. Misconfiguration can lock users out if HTTPS fails +3. It should be coordinated with SSL certificate management +4. It may need different settings for staging vs production + +### Header Ownership Summary + +| Header | Owner | Reason | +|--------|-------|--------| +| X-Content-Type-Options | Application | Version controlled, consistent behavior | +| X-Frame-Options | Application | May need app-specific logic in future | +| Referrer-Policy | Application | Version controlled, consistent behavior | +| Permissions-Policy | Application | App knows which features it needs | +| Content-Security-Policy | Application | Will require nonces (dynamic per-request) | +| Strict-Transport-Security | Nginx | Infrastructure concern | +| Cache-Control (static) | Nginx | CDN/proxy optimization | + +## Verifying Headers + +After deployment, verify headers are present and not duplicated: + +```bash +# Check headers on a page +curl -I https://your-domain.com/trending + +# Expected output should include: +# X-Content-Type-Options: nosniff +# X-Frame-Options: SAMEORIGIN +# Referrer-Policy: strict-origin-when-cross-origin +# Permissions-Policy: camera=(), microphone=(), geolocation=() +# Strict-Transport-Security: max-age=31536000; includeSubDomains + +# Check for duplicate headers (problem if same header appears twice) +curl -I https://your-domain.com/trending 2>/dev/null | grep -i "x-frame-options" +# Should show only ONE line +``` + +## Troubleshooting + +### Duplicate Headers + +If you see the same header twice in responses: +1. Check if both nginx and the application are setting it +2. Remove the header from nginx configuration +3. Restart nginx and verify + +### Missing Headers + +If headers are missing: +1. Verify the Next.js application is running the latest version +2. Check that `next.config.js` includes the `headers()` function +3. Ensure nginx is not stripping headers with `proxy_hide_header` + +### Header Conflicts + +If nginx is overwriting application headers: +1. Remove the conflicting `add_header` directives from nginx +2. Use `proxy_pass_header` if needed to preserve specific headers -- GitLab From a1f30e57bcdc78829b1ae9dc8f9b65fd5580fab5 Mon Sep 17 00:00:00 2001 From: Gandalf Date: Wed, 31 Dec 2025 00:28:48 +0100 Subject: [PATCH 2/2] feat: Add additional security headers and disable X-Powered-By Based on comparison with Condenser (hive.blog, wallet.hive.blog): - Add X-DNS-Prefetch-Control: off (privacy protection) - Add X-Download-Options: noopen (IE security) - Set poweredByHeader: false (don't expose Next.js) - Update documentation with new headers These headers match what Condenser wallet currently sets. --- apps/blog/next.config.js | 9 +++++++++ apps/wallet/next.config.js | 9 +++++++++ docs/security-headers.md | 11 +++++++++++ 3 files changed, 29 insertions(+) diff --git a/apps/blog/next.config.js b/apps/blog/next.config.js index a35ec3620..5b054d7e5 100644 --- a/apps/blog/next.config.js +++ b/apps/blog/next.config.js @@ -21,6 +21,14 @@ const securityHeaders = [ key: 'X-Frame-Options', value: 'SAMEORIGIN' }, + { + key: 'X-DNS-Prefetch-Control', + value: 'off' + }, + { + key: 'X-Download-Options', + value: 'noopen' + }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' @@ -34,6 +42,7 @@ const securityHeaders = [ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + poweredByHeader: false, // Don't expose X-Powered-By: Next.js output: 'standalone', swcMinify: false, basePath: basePath, diff --git a/apps/wallet/next.config.js b/apps/wallet/next.config.js index 2c556c54d..cdb6b9227 100644 --- a/apps/wallet/next.config.js +++ b/apps/wallet/next.config.js @@ -21,6 +21,14 @@ const securityHeaders = [ key: 'X-Frame-Options', value: 'SAMEORIGIN' }, + { + key: 'X-DNS-Prefetch-Control', + value: 'off' + }, + { + key: 'X-Download-Options', + value: 'noopen' + }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' @@ -34,6 +42,7 @@ const securityHeaders = [ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + poweredByHeader: false, // Don't expose X-Powered-By: Next.js output: 'standalone', swcMinify: false, // basePath is set at build time from NEXT_PUBLIC_BASE_PATH env variable diff --git a/docs/security-headers.md b/docs/security-headers.md index d52416f79..6a947af05 100644 --- a/docs/security-headers.md +++ b/docs/security-headers.md @@ -14,9 +14,13 @@ The following headers are configured in `apps/blog/next.config.js` and `apps/wal |--------|-------|---------| | X-Content-Type-Options | `nosniff` | Prevents MIME-sniffing attacks | | X-Frame-Options | `SAMEORIGIN` | Prevents clickjacking by restricting iframe embedding | +| X-DNS-Prefetch-Control | `off` | Prevents DNS prefetching to protect privacy | +| X-Download-Options | `noopen` | Prevents IE from executing downloads in site context | | Referrer-Policy | `strict-origin-when-cross-origin` | Controls referrer information sent to other sites | | Permissions-Policy | `camera=(), microphone=(), geolocation=()` | Disables unused browser features | +Additionally, `poweredByHeader: false` is set in Next.js config to prevent exposing `X-Powered-By: Next.js`. + ### Future Headers (Planned) | Header | Status | Notes | @@ -87,6 +91,8 @@ Strict-Transport-Security (HSTS) is an infrastructure concern because: |--------|-------|--------| | X-Content-Type-Options | Application | Version controlled, consistent behavior | | X-Frame-Options | Application | May need app-specific logic in future | +| X-DNS-Prefetch-Control | Application | Privacy protection, version controlled | +| X-Download-Options | Application | Security, version controlled | | Referrer-Policy | Application | Version controlled, consistent behavior | | Permissions-Policy | Application | App knows which features it needs | | Content-Security-Policy | Application | Will require nonces (dynamic per-request) | @@ -104,9 +110,14 @@ curl -I https://your-domain.com/trending # Expected output should include: # X-Content-Type-Options: nosniff # X-Frame-Options: SAMEORIGIN +# X-DNS-Prefetch-Control: off +# X-Download-Options: noopen # Referrer-Policy: strict-origin-when-cross-origin # Permissions-Policy: camera=(), microphone=(), geolocation=() # Strict-Transport-Security: max-age=31536000; includeSubDomains +# +# Should NOT include: +# X-Powered-By: Next.js # Check for duplicate headers (problem if same header appears twice) curl -I https://your-domain.com/trending 2>/dev/null | grep -i "x-frame-options" -- GitLab