Skip to main content

Security Headers Reference

The RCB platform sets HTTP security headers at two distinct layers: the Spring Security filter chain (for all /api/** responses) and nginx (for the React SPA and static assets). Each layer is tuned independently because their content requirements differ — the SPA needs unsafe-inline for MUI's emotion CSS engine while the API never serves inline scripts.


Layer 1 — Spring Security (API Responses)

All API responses from the Spring Boot backend include the following headers, configured via the .headers() chain in SecurityConfig.java.

Full Headers Configuration

http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives(
"default-src 'self'; " +
"img-src 'self' res.cloudinary.com data: blob:; " +
"script-src 'self'; " +
"style-src 'self' 'unsafe-inline'; " +
"font-src 'self' data:; " +
"connect-src 'self'; " +
"frame-ancestors 'none';"
)
)
.frameOptions(frame -> frame.deny())
.contentTypeOptions(Customizer.withDefaults())
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
)
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
.permissionsPolicy(permissions -> permissions
.policy("camera=(), microphone=(), geolocation=(self), payment=(), usb=(), fullscreen=(self)")
)
);

Header Values and Rationale

HeaderValueOWASP CategoryPurpose
Content-Security-Policydefault-src 'self'; img-src 'self' res.cloudinary.com data: blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';A05 Security MisconfigurationRestricts resource loading to trusted origins; frame-ancestors 'none' prevents clickjacking
X-Frame-OptionsDENYA05 Security MisconfigurationLegacy clickjacking prevention for older browsers that do not support CSP frame-ancestors
X-Content-Type-OptionsnosniffA05 Security MisconfigurationPrevents browsers from MIME-sniffing responses away from the declared Content-Type
Strict-Transport-Securitymax-age=31536000; includeSubDomainsA02 Cryptographic FailuresForces HTTPS for 1 year on all subdomains; prevents SSL stripping attacks
Referrer-Policystrict-origin-when-cross-originA05 Security MisconfigurationSends full path only for same-origin requests; only the origin for cross-origin; nothing for downgrade
Permissions-Policycamera=(), microphone=(), geolocation=(self), payment=(), usb=(), fullscreen=(self)A05 Security MisconfigurationDisables unused browser APIs (camera, mic, NFC, payment) at the browser level

Layer 2 — nginx (SPA and Static Responses)

The nginx.conf in the rcb-frontend container sets a separate add_header block for all responses from the React application and static files.

nginx Security Headers Block

# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self), payment=(), usb=(), fullscreen=(self)" always;
add_header Content-Security-Policy
"default-src 'self'; "
"script-src 'self' 'unsafe-inline'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' res.cloudinary.com data: blob:; "
"font-src 'self' data:; "
"connect-src 'self' https://auth.rcb.bg; "
"frame-ancestors 'none';"
always;

Why nginx CSP Differs from the API CSP

DirectiveAPI (Spring Security)SPA (nginx)Reason for Difference
script-src'self''self' 'unsafe-inline'React is server-rendered as static JS bundles; no inline scripts needed. However, some build tool polyfills and MUI may inject inline event handlers in older setups. Evaluate removing unsafe-inline after full Vite migration.
style-src'self' 'unsafe-inline''self' 'unsafe-inline'MUI requires unsafe-inline — emotion CSS-in-JS injects <style> tags at runtime. See Tightening CSP for MUI.
connect-src'self''self' https://auth.rcb.bgThe SPA makes XHR/fetch calls to Keycloak for token refresh. The API never calls Keycloak from the browser.

Request / Response Flow

Header Precedence

nginx does not override Spring Security headers for API routes — it only proxy_pass them through. Spring Security headers take precedence for all /api/** responses. nginx only adds its own headers for non-proxied responses (SPA HTML, static JS/CSS/images).


Verifying Headers in Production

Quick Check — All Security Headers

curl -sI https://rcb.bg/api/v1/home \
| grep -iE "x-frame|content-security|x-content-type|strict-transport|referrer|permissions"

Expected output (trimmed):

content-security-policy: default-src 'self'; img-src 'self' res.cloudinary.com data: blob:; ...
x-frame-options: DENY
x-content-type-options: nosniff
strict-transport-security: max-age=31536000; includeSubDomains
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(self), payment=(), usb=(), fullscreen=(self)

Check SPA Headers

curl -sI https://rcb.bg/ \
| grep -iE "x-frame|content-security|x-content-type|referrer|permissions"

Automated Security Headers Check (CI)

The weekly security-scan.yml includes a security-headers job that runs the curl check and fails the workflow if any required header is missing. See Weekly Security Scan.


OWASP References

The following OWASP Top 10 (2021) categories are addressed by these headers:

OWASP CategoryHeaders That Address It
A02 — Cryptographic FailuresStrict-Transport-Security (forces HTTPS)
A05 — Security MisconfigurationAll headers — CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy
A07 — Identification and Authentication Failuresframe-ancestors 'none' (prevents clickjacking on login form)

Tightening CSP for MUI with Nonces

Currently style-src 'unsafe-inline' is required because MUI's emotion library injects styles at runtime. This is the most common XSS mitigation gap in React applications using CSS-in-JS.

Future hardening path:

  1. Configure emotion to use a nonce attribute: MUI CSP Guide
  2. Generate a per-request nonce in Spring Boot and pass it as a response header
  3. Configure nginx to forward the nonce to the React app via a meta tag injection or SSR
  4. Replace 'unsafe-inline' with 'nonce-<value>' in the CSP style-src directive

This is tracked as a future hardening task — it requires coordination between FE (emotion config) and BE (nonce generation middleware).


References