This document describes every HTTP security header configured for the VacciChain stack, the purpose of each header, the exact value in use, and instructions for updating headers when new resources are added.
Headers are applied at two layers:
| Layer | Where configured | Applies to |
|---|---|---|
| nginx | frontend/nginx.conf |
All responses served by the frontend container (HTML, JS, CSS, API proxied responses) |
| Express | backend/src/app.js (via Helmet) |
All responses from the backend API when accessed directly (port 4000) |
In the Docker Compose setup, the browser only talks to nginx (port 3000). nginx proxies /auth/, /vaccination/, and /verify/ to the backend, so the nginx headers are the ones the browser actually sees. The Express headers provide defence-in-depth for direct API access and non-Docker deployments.
Purpose CSP is the primary defence against Cross-Site Scripting (XSS) and data-injection attacks. It tells the browser which sources are allowed to load scripts, styles, images, fonts, and other sub-resources. Any resource not matching the policy is blocked before it executes.
Configured value
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://horizon-testnet.stellar.org https://horizon.stellar.org
https://soroban-testnet.stellar.org https://mainnet.sorobanrpc.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
Directive-by-directive justification
| Directive | Value | Reason |
|---|---|---|
default-src |
'self' |
Catch-all fallback. Any sub-resource type not explicitly listed falls back to same-origin only. |
script-src |
'self' |
All JavaScript is bundled by Vite and served from the same origin. No CDN scripts, no inline <script> blocks, no eval. |
style-src |
'self' 'unsafe-inline' |
Vite injects critical CSS as inline <style> tags during development and in some production builds. 'unsafe-inline' is required for those injected styles. If you migrate to CSS-in-JS or extract all styles to .css files, remove 'unsafe-inline' and add a nonce or hash instead. |
img-src |
'self' data: |
The UI uses data: URIs for SVG icons and placeholder images rendered inline by React components. No external image CDN is used. |
font-src |
'self' |
No external font services (Google Fonts, etc.) are used. All fonts are bundled locally. |
connect-src |
'self' + Stellar endpoints |
The Freighter browser extension and the frontend's useFreighter hook make direct XHR/fetch calls to Stellar Horizon and Soroban RPC endpoints for transaction simulation and submission. Both testnet and mainnet URLs are listed so the same build works in both environments. |
frame-ancestors |
'none' |
Prevents the app from being embedded in any <iframe>, <frame>, or <object>. Equivalent to X-Frame-Options: DENY but more expressive and not limited to top-level frames. |
base-uri |
'self' |
Prevents attackers from injecting a <base> tag that would redirect all relative URLs to an attacker-controlled origin. |
form-action |
'self' |
Restricts where HTML forms can submit. The app uses fetch-based API calls rather than form submissions, but this directive closes the vector entirely. |
Purpose
Prevents clickjacking by controlling whether the page can be rendered inside a frame on another origin. This header is understood by older browsers that do not support frame-ancestors in CSP.
Configured value
X-Frame-Options: DENY
DENY refuses framing from any origin, including the same origin. SAMEORIGIN would allow same-origin framing, which is unnecessary for VacciChain — no page in the app is designed to be embedded.
Purpose
Stops browsers from MIME-sniffing a response away from the declared Content-Type. Without this header, a browser might execute a JavaScript file served as text/plain, enabling content-injection attacks.
Configured value
X-Content-Type-Options: nosniff
nosniff is the only valid value. It instructs the browser to honour the server-declared content type and never guess.
Purpose
Forces all future connections to the site to use HTTPS, even if the user types http:// or follows an http:// link. After the first HTTPS visit, the browser will refuse to make plain-HTTP requests for the duration of max-age.
Configured value
Strict-Transport-Security: max-age=31536000; includeSubDomains
| Parameter | Value | Reason |
|---|---|---|
max-age |
31536000 (1 year) |
Standard recommendation. The browser caches this policy for one year. |
includeSubDomains |
present | Ensures subdomains (e.g. api.vaccichain.example.com) are also covered. |
preload |
absent | Not included until the domain is submitted to the HSTS preload list. Add preload only after the domain is registered there. |
Note for local development: HSTS has no effect over plain HTTP (localhost). It only activates when the response is served over HTTPS. Do not set this header in development environments to avoid locking the browser into HTTPS for
localhost.
Purpose
Controls how much referrer information is included in the Referer header when navigating away from the app. Leaking full URLs in the Referer header can expose patient wallet addresses or session tokens embedded in query strings.
Configured value
Referrer-Policy: strict-origin-when-cross-origin
| Scenario | What is sent |
|---|---|
| Same-origin navigation | Full URL |
| Cross-origin navigation over HTTPS→HTTPS | Origin only (e.g. https://vaccichain.example.com) |
| Cross-origin navigation over HTTPS→HTTP | Nothing (referrer stripped entirely) |
This is the browser default in modern browsers, but setting it explicitly ensures consistent behaviour across all browsers and prevents downgrades.
Purpose Disables browser features that the app does not use. Restricting unused APIs reduces the attack surface if an XSS payload does execute — the attacker cannot access the camera, microphone, geolocation, or payment APIs.
Configured value
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
| Feature | Value | Reason |
|---|---|---|
camera |
() (blocked) |
Not used by the app. |
microphone |
() (blocked) |
Not used by the app. |
geolocation |
() (blocked) |
Not used by the app. |
payment |
() (blocked) |
Payments are handled on-chain via Stellar, not via the Payment Request API. |
usb |
() (blocked) |
No hardware wallet USB access is required; Freighter handles signing. |
Purpose Enables the legacy XSS auditor built into older versions of Internet Explorer and early Chrome. Modern browsers have removed this auditor in favour of CSP, but the header is included for compatibility with older clients.
Configured value
X-XSS-Protection: 1; mode=block
mode=block instructs the browser to block the page entirely rather than attempt to sanitise and render it when an XSS attack is detected. This header has no effect on modern browsers (Chrome 78+, Firefox, Safari) where the auditor was removed.
Add a add_header directive inside the server block. All headers listed above should be present:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://horizon-testnet.stellar.org https://horizon.stellar.org https://soroban-testnet.stellar.org https://mainnet.sorobanrpc.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
try_files $uri $uri/ /index.html;
}
location /auth/ {
proxy_pass http://backend:4000;
}
location /vaccination/ {
proxy_pass http://backend:4000;
}
location /verify/ {
proxy_pass http://backend:4000;
}
}The always flag ensures headers are sent on error responses (4xx, 5xx) as well as successful ones.
Install Helmet, which sets all of the above headers automatically:
npm install helmetThen add it as the first middleware in app.js:
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:"],
fontSrc: ["'self'"],
connectSrc: [
"'self'",
"https://horizon-testnet.stellar.org",
"https://horizon.stellar.org",
"https://soroban-testnet.stellar.org",
"https://mainnet.sorobanrpc.com",
],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
},
frameguard: { action: 'deny' },
noSniff: true,
hsts: { maxAge: 31536000, includeSubDomains: true },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
permittedCrossDomainPolicies: false,
}));- Add the script's origin to
script-srcin both nginx and Express:script-src 'self' https://cdn.example.com; - If the script is loaded inline (e.g. a
<script>tag inindex.html), generate a SHA-256 hash of the exact script content and add it instead of'unsafe-inline':Never addscript-src 'self' 'sha256-<base64-hash>';'unsafe-inline'toscript-src— it defeats XSS protection entirely.
- Add the font CDN origin to
style-srcandfont-src:style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com;
- Add the image origin to
img-src:img-src 'self' data: https://images.example.com;
The connect-src directive must include every Horizon and Soroban RPC URL the frontend fetches directly. When adding a new network (e.g. Futurenet):
- Add the new endpoint URL to
connect-srcin both nginx and Express:connect-src 'self' ... https://rpc-futurenet.stellar.org;
If the backend moves to a subdomain (e.g. api.vaccichain.example.com), add it to connect-src:
connect-src 'self' https://api.vaccichain.example.com ...;
After updating headers, verify them with:
- Browser DevTools → Network tab → select any response → Headers panel
- securityheaders.com — paste your public URL for a graded report
- Mozilla Observatory — comprehensive scan including CSP analysis
| Header | Value | Protects against |
|---|---|---|
Content-Security-Policy |
See directives above | XSS, data injection, clickjacking (via frame-ancestors) |
X-Frame-Options |
DENY |
Clickjacking (legacy browser fallback) |
X-Content-Type-Options |
nosniff |
MIME-type confusion attacks |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
SSL stripping, protocol downgrade attacks |
Referrer-Policy |
strict-origin-when-cross-origin |
Referrer leakage of sensitive URL parameters |
Permissions-Policy |
camera, mic, geo, payment, usb all blocked | Abuse of browser APIs by injected scripts |
X-XSS-Protection |
1; mode=block |
Legacy XSS auditor (IE/old Chrome) |
VacciChain implements comprehensive security headers to protect against common web vulnerabilities including XSS (Cross-Site Scripting), clickjacking, MIME sniffing, and other attacks. This is especially critical for a blockchain application that handles wallet interactions and sensitive health data.
Purpose: Prevents XSS attacks by controlling which resources can be loaded and executed.
Frontend Configuration (Nginx):
Content-Security-Policy: default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://horizon-testnet.stellar.org https://soroban-testnet.stellar.org https://horizon.stellar.org https://soroban.stellar.org;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
Backend Configuration:
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
Why This Matters for VacciChain:
- Prevents malicious scripts from intercepting Freighter wallet signing prompts
- Blocks unauthorized connections to external APIs
- Protects against injection attacks that could steal vaccination records
Production Notes:
unsafe-inlineis used for React inline styles in development- Consider using a CSS-in-JS solution or nonces to remove
unsafe-inlinein production connect-srcexplicitly allows Stellar Horizon and Soroban RPC endpoints
Value: DENY
Purpose: Prevents clickjacking attacks by disallowing the page to be embedded in frames/iframes.
Why This Matters for VacciChain:
- Prevents attackers from overlaying fake UI elements on top of wallet signing prompts
- Protects against UI redressing attacks that could trick users into signing malicious transactions
Value: nosniff
Purpose: Prevents browsers from MIME-sniffing responses, forcing them to respect the declared Content-Type.
Why This Matters for VacciChain:
- Prevents browsers from interpreting JSON responses as HTML/JavaScript
- Blocks attacks where malicious content is disguised with incorrect MIME types
Value: strict-origin-when-cross-origin
Purpose: Controls how much referrer information is sent with requests.
Behavior:
- Same-origin requests: Full URL is sent
- Cross-origin requests: Only origin (no path/query) is sent
- HTTPS → HTTP: No referrer is sent
Why This Matters for VacciChain:
- Prevents leaking sensitive URLs (e.g., with patient IDs) to external sites
- Maintains privacy while allowing legitimate analytics
Value: 1; mode=block
Purpose: Enables browser's built-in XSS filter (legacy, but still useful for older browsers).
Note: Modern browsers rely on CSP, but this provides defense-in-depth for older browsers.
Value: max-age=31536000; includeSubDomains; preload
Purpose: Forces browsers to only connect via HTTPS.
Status:
⚠️ Disabled in development (commented out innginx.conf)- ✅ Enabled in production (
nginx.production.conf)
Why This Matters for VacciChain:
- Prevents man-in-the-middle attacks
- Protects wallet private keys and authentication tokens in transit
- Critical for mainnet deployment
Production Deployment:
- Ensure HTTPS is working properly
- Test with a short
max-agefirst (e.g., 300 seconds) - Gradually increase to 31536000 (1 year)
- Consider HSTS preloading: https://hstspreload.org/
Value: geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()
Purpose: Disables unnecessary browser features to reduce attack surface.
Why This Matters for VacciChain:
- VacciChain doesn't need geolocation, camera, or other device features
- Reduces risk of permission-based attacks
- Improves privacy
Development: frontend/nginx.conf
- Includes
unsafe-inlinefor React development - HSTS disabled
- Suitable for local development and testnet
Production: frontend/nginx.production.conf
- Stricter CSP (no
unsafe-inline) - HSTS enabled
- Use for mainnet deployment
Middleware: backend/src/middleware/securityHeaders.js
- Applied to all API responses
- Restrictive CSP for JSON-only API
- Automatically loaded in
app.js
Linux/macOS:
chmod +x scripts/test-security-headers.sh
./scripts/test-security-headers.shWindows:
.\scripts\test-security-headers.ps1Custom URLs:
# Linux/macOS
./scripts/test-security-headers.sh https://staging.vaccichain.com https://api.vaccichain.com
# Windows
.\scripts\test-security-headers.ps1 -FrontendUrl "https://staging.vaccichain.com" -BackendUrl "https://api.vaccichain.com"Using curl:
# Frontend
curl -I http://localhost:3000
# Backend
curl -I http://localhost:4000/healthUsing browser DevTools:
- Open VacciChain in browser
- Open DevTools (F12)
- Go to Network tab
- Refresh page
- Click on the main document request
- Check "Response Headers" section
SecurityHeaders.com:
- Deploy to a publicly accessible URL
- Visit https://securityheaders.com/
- Enter your URL
- Target: Grade A or higher
Mozilla Observatory:
- Visit https://observatory.mozilla.org/
- Enter your URL
- Target: A+ rating
Expected Results:
- ✅ Content-Security-Policy: Present
- ✅ X-Frame-Options: DENY
- ✅ X-Content-Type-Options: nosniff
- ✅ Referrer-Policy: strict-origin-when-cross-origin
- ✅ Permissions-Policy: Present
⚠️ Strict-Transport-Security: Only in production with HTTPS
Symptom: Console errors like "Refused to execute inline script"
Solution:
- Move inline scripts to external
.jsfiles - Use nonces or hashes for necessary inline scripts
- For React: Use styled-components or CSS modules instead of inline styles
Symptom: Images, fonts, or API calls fail to load
Solution:
- Add the domain to the appropriate CSP directive
- Example:
img-src 'self' https://trusted-cdn.com - Be specific - avoid using wildcards like
*
Symptom: Wallet connection or signing fails
Solution:
- Ensure
connect-srcincludes Stellar endpoints - Check browser console for CSP violations
- Freighter injects scripts - may need
script-srcadjustments
Symptom: Browser forces HTTPS on localhost
Solution:
- Clear HSTS settings in browser:
- Chrome:
chrome://net-internals/#hsts→ Delete domain - Firefox: Delete
SiteSecurityServiceState.txtin profile folder
- Chrome:
- Use different domain for development (e.g.,
local.vaccichain.test)
- Test headers after every deployment
- Run security scanners monthly
- Review CSP violations in browser console
Consider adding CSP reporting in production:
add_header Content-Security-Policy "...; report-uri /csp-report";- Test in staging first
- Use
Content-Security-Policy-Report-Onlyto test without blocking - Monitor for violations before enforcing
- Review OWASP recommendations: https://owasp.org/www-project-secure-headers/
- Follow Mozilla guidelines: https://infosec.mozilla.org/guidelines/web_security
- Update headers as new threats emerge
- Switch to
nginx.production.conf - Enable HTTPS with valid SSL certificate
- Test HSTS with short
max-agefirst - Remove
unsafe-inlinefrom CSP if possible - Test all functionality (especially Freighter wallet)
- Run securityheaders.com scan (target: Grade A)
- Run Mozilla Observatory scan (target: A+)
- Set up CSP violation reporting
- Document any CSP exceptions and why they're needed
- Schedule regular security header audits
- OWASP Secure Headers Project
- MDN Web Security
- Content Security Policy Reference
- SecurityHeaders.com
- Mozilla Observatory
- HSTS Preload
For security concerns or questions:
- Review this documentation
- Check browser console for CSP violations
- Test with provided scripts
- Contact the security team for production deployments