This document describes the comprehensive HTTP security headers implementation for the Soroban Security Scanner application. The implementation protects against XSS, clickjacking, MIME sniffing, and other common web vulnerabilities.
- Architecture
- Content Security Policy (CSP)
- HTTP Strict Transport Security (HSTS)
- Other Security Headers
- Adding New External Resources
- Testing and Validation
- Troubleshooting
Security headers are implemented at two levels:
The primary implementation uses Next.js Edge Middleware to set headers on all dynamic routes. This approach:
- Generates unique cryptographic nonces per request for CSP
- Sets environment-specific headers (e.g., HSTS only in production)
- Applies to all routes except static assets
Static headers that don't require per-request logic are set in the Next.js configuration:
X-DNS-Prefetch-ControlX-XSS-Protection
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self';
connect-src 'self' https://horizon.stellar.org https://horizon-testnet.stellar.org https://horizon-futurenet.stellar.org ws://localhost:* wss://*.stellar.org;
frame-src 'none';
frame-ancestors 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
block-all-mixed-content;
Fallback for all resource types. Only allows resources from the same origin.
- 'self': Allows scripts from the same origin
- 'nonce-{RANDOM}': Allows inline scripts with matching nonce attribute
- No unsafe-eval: Prevents
eval()and similar dangerous functions - No unsafe-inline: Prevents inline scripts without nonces
Why nonces? Nonces provide a secure way to allow specific inline scripts while blocking XSS attacks. Each request gets a unique cryptographic nonce that must match between the CSP header and script tags.
- 'self': Allows stylesheets from the same origin
- 'unsafe-inline': Required for:
- Next.js styled-jsx
- Tailwind CSS utility classes
- React inline styles
Note: unsafe-inline for styles is acceptable because CSS injection is significantly less dangerous than JavaScript injection. CSS cannot execute arbitrary code or steal sensitive data.
- 'self': Same-origin images
- data:: Base64-encoded inline images
- blob:: Dynamically generated images (canvas, file uploads)
Only allows fonts from the same origin. No external font CDNs are used.
Controls where the application can make network requests:
- 'self': API calls to the same origin
- https://horizon.stellar.org: Stellar mainnet Horizon API
- https://horizon-testnet.stellar.org: Stellar testnet Horizon API
- https://horizon-futurenet.stellar.org: Stellar futurenet Horizon API
- ws://localhost:*: Local WebSocket development
- wss://*.stellar.org: Stellar WebSocket endpoints
Completely disables embedding of frames/iframes. The application does not use iframes.
Prevents this site from being embedded in iframes on other sites. This is the CSP equivalent of X-Frame-Options: DENY.
Disables plugins like Flash, Java applets, and other legacy embedded objects.
Restricts the <base> tag to prevent base tag hijacking attacks.
Restricts form submissions to the same origin.
Automatically upgrades HTTP requests to HTTPS in supporting browsers.
Prevents loading any HTTP resources when the page is served over HTTPS.
-
Middleware generates nonce (
frontend/middleware.ts):const nonce = randomBytes(16).toString('base64');
-
Nonce added to CSP header:
script-src 'self' 'nonce-{nonce}';
-
Nonce stored in response header:
response.headers.set('x-nonce', nonce);
-
Layout retrieves nonce (
frontend/app/layout.tsx):const nonce = headers().get('x-nonce') || '';
-
Next.js automatically applies nonce to its own scripts
-
Custom inline scripts must include the nonce attribute:
<script nonce={nonce}> // Your code here </script>
- Unique per request: Each page load gets a new nonce
- Cryptographically random: Uses
crypto.randomBytes(16)(128 bits of entropy) - Unpredictable: Attackers cannot guess or reuse nonces
- Automatic: Next.js handles nonce injection for framework scripts
-
Development: Uses
Content-Security-Policy-Report-Onlyheader- Violations are logged to console but not blocked
- Allows testing without breaking functionality
-
Production: Uses enforcing
Content-Security-Policyheader- Violations are blocked
- Provides actual security protection
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
- max-age=31536000: Enforce HTTPS for 1 year (365 days)
- includeSubDomains: Apply to all subdomains
- preload: Eligible for browser HSTS preload list
-
HTTPS Only: HSTS is only set on HTTPS responses. Setting it on HTTP would be ineffective and could cause issues.
-
Production Only: HSTS is only enabled in production (
NODE_ENV=production) to avoid issues in local development. -
Subdomain Consideration: The
includeSubDomainsdirective means ALL subdomains must support HTTPS. If you have any HTTP-only subdomains, remove this directive. -
Preload List: The
preloaddirective indicates intent to submit to the HSTS preload list. This is optional but recommended for maximum security.
To submit your domain to the HSTS preload list:
-
Ensure HSTS header is set with:
max-ageof at least 31536000 (1 year)includeSubDomainsdirectivepreloaddirective
-
Verify all subdomains support HTTPS
-
Submit at https://hstspreload.org/
-
Wait for inclusion in browser preload lists (can take months)
X-Frame-Options: DENY
Prevents the site from being embedded in iframes, protecting against clickjacking attacks.
- DENY: Never allow framing
- Redundant with CSP:
frame-ancestors 'none'provides the same protection, butX-Frame-Optionsis kept for older browser compatibility
X-Content-Type-Options: nosniff
Prevents browsers from MIME-sniffing responses away from the declared Content-Type.
- Always set: No configuration needed
- Prevents: Attacks where an attacker uploads a file with malicious content disguised as a safe MIME type
Referrer-Policy: strict-origin-when-cross-origin
Controls how much referrer information is sent with requests:
- Same-origin requests: Full URL (including path and query)
- Cross-origin HTTPS requests: Origin only (no path/query)
- Cross-origin HTTP requests: No referrer
Why this policy?
- Protects sensitive URL parameters from leaking to third parties
- Still provides useful analytics data for same-origin requests
- Balances privacy and functionality
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()
Disables browser features the application doesn't use:
- camera=(): No camera access
- microphone=(): No microphone access
- geolocation=(): No location access
- payment=(): No Payment Request API
- usb=(): No WebUSB API
- interest-cohort=(): Opt out of Google FLoC/Topics API
Note: If you need to enable a feature (e.g., camera for KYC), update the policy:
camera=(self "https://kyc.example.com")
Cross-Origin-Opener-Policy: same-origin
Isolates the browsing context, preventing other origins from accessing the window object.
Cross-Origin-Resource-Policy: same-origin
Prevents cross-origin resource loading.
Not currently set because it would block Stellar Horizon API calls that don't send CORP headers.
To enable COEP:
- Verify all external resources send
Cross-Origin-Resource-Policy: cross-origin - Add to middleware:
'Cross-Origin-Embedder-Policy': 'require-corp'
When you need to add a new external resource (API, CDN, font, etc.), follow these steps:
Determine which CSP directive applies:
- JavaScript:
script-src - CSS:
style-src - Images:
img-src - Fonts:
font-src - API/WebSocket:
connect-src - Iframes:
frame-src
Edit frontend/middleware.ts and add the origin to the appropriate directive:
const directives: CSPDirectives = {
// ... existing directives ...
'connect-src': [
"'self'",
'https://horizon.stellar.org',
'https://new-api.example.com', // ← Add new origin here
],
};-
Start the development server:
cd frontend npm run dev -
Open browser DevTools Console
-
Load the page that uses the new resource
-
Check for CSP violations:
- Look for red errors starting with "Refused to load..."
- Verify the resource loads successfully
Run the security header tests:
cd frontend
npm test -- security-headersUpdate this document with:
- Why the origin was added
- What functionality requires it
- Any security considerations
If you need to add Google Fonts:
-
Update CSP in
frontend/middleware.ts:'style-src': [ "'self'", "'unsafe-inline'", 'https://fonts.googleapis.com', // ← Google Fonts CSS ], 'font-src': [ "'self'", 'https://fonts.gstatic.com', // ← Google Fonts files ],
-
Add to HTML:
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
-
Test: Verify fonts load without CSP violations
Run the test suite:
cd frontend
npm testTests cover:
- ✅ All security headers are present
- ✅ CSP includes all required directives
- ✅ Nonces are unique per request
- ✅ HSTS is set correctly in production
- ✅ No unsafe-eval in CSP
- ✅ All browser features are disabled in Permissions-Policy
-
Start the application:
cd frontend npm run dev -
Open DevTools Console (F12)
-
Load each major page:
- Home page
- Authentication pages
- Scanner interface
- Settings panel
-
Check for CSP violations:
- Look for red errors starting with "Refused to..."
- There should be ZERO CSP violations
-
Verify functionality:
- All images load
- All styles apply
- All JavaScript works
- API calls succeed
- WebSocket connections work
- Deploy to staging/production
- Visit https://observatory.mozilla.org
- Enter your URL
- Run scan
- Target: Grade A or higher
Current Score: [To be filled after deployment]
- Visit https://securityheaders.com
- Enter your URL
- Run scan
- Target: Grade A or higher
Current Score: [To be filled after deployment]
- Visit https://csp-evaluator.withgoogle.com
- Paste your CSP policy
- Review findings
- Target: No HIGH severity issues
Current Score: [To be filled after deployment]
Before deploying to production:
- All automated tests pass
- Zero CSP violations in browser console on all pages
- All external resources load correctly
- Authentication flows work end-to-end
- API calls to Stellar Horizon succeed
- WebSocket connections establish successfully
- Mozilla Observatory grade A or higher
- SecurityHeaders.com grade A or higher
- CSP Evaluator shows no HIGH severity issues
- HSTS header present in production
- All security headers present on all routes
Error:
Refused to load the script 'https://example.com/script.js' because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-...'"
Solution:
- Identify the blocked origin:
https://example.com - Add to
script-srcinfrontend/middleware.ts:'script-src': [ "'self'", `'nonce-${nonce}'`, 'https://example.com', // ← Add this ],
Error:
Refused to connect to 'https://api.example.com' because it violates the following Content Security Policy directive: "connect-src 'self' ..."
Solution:
- Add the API origin to
connect-src:'connect-src': [ "'self'", 'https://api.example.com', // ← Add this // ... other origins ],
Error:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-...'"
Solution:
-
Get the nonce in your component:
import { headers } from 'next/headers'; const nonce = headers().get('x-nonce') || '';
-
Add nonce to script tag:
<script nonce={nonce}> // Your code </script>
Expected behavior: HSTS is intentionally disabled in development to avoid issues with localhost.
Solution: Test HSTS in a staging or production environment with HTTPS.
Check:
- Are fonts from an external CDN? Add the CDN to
font-src - Are fonts self-hosted? Ensure they're in the
publicdirectory - Check browser console for CSP violations
Check:
- External images? Add the domain to
img-src - Base64 images? Ensure
data:is inimg-src - Blob URLs? Ensure
blob:is inimg-src
Check:
- Is the API origin in
connect-src? - Is CORS configured on the API server?
- Check browser console for CSP violations
Check:
- Is the WebSocket origin in
connect-src? - Use
ws://for HTTP andwss://for HTTPS - Ensure the WebSocket server is running
This implementation follows defense-in-depth principles:
- CSP: Primary defense against XSS
- X-Frame-Options: Backup for older browsers
- HSTS: Enforces HTTPS
- X-Content-Type-Options: Prevents MIME confusion
- Referrer-Policy: Protects sensitive URLs
- Permissions-Policy: Reduces attack surface
Schedule regular security audits:
- Monthly: Run online scanners (Observatory, SecurityHeaders.com)
- Quarterly: Review CSP policy for unnecessary origins
- Annually: Full security audit by security professionals
If a CSP violation is reported in production:
- Investigate: Is it a legitimate resource or an attack?
- Legitimate: Add the origin to CSP
- Attack: Investigate how the malicious code was injected
- Document: Record the incident and response
Security headers evolve. Stay informed:
- Subscribe to OWASP Secure Headers Project
- Follow Mozilla Web Security Guidelines
- Monitor CSP specification updates
- OWASP Secure Headers Project
- MDN Web Security
- Content Security Policy Reference
- HSTS Preload List
- Mozilla Observatory
- SecurityHeaders.com
- CSP Evaluator
- Implemented CSP with nonces
- Added HSTS with preload
- Set all recommended security headers
- Created comprehensive test suite
- Documented all policies and procedures
Scan Results:
- Mozilla Observatory: [To be filled]
- SecurityHeaders.com: [To be filled]
- CSP Evaluator: [To be filled]