PS Product SecurityKnowledge Base

๐ŸŒ Web-Server Security Controls: HTTPS, CORS, CSP, HSTS, and Headers on Apache and Nginx

Intro: Not every web-security control belongs in application code. A large part of real production posture is determined by what the edge, reverse proxy, and web server do with TLS, CORS, security headers, content types, and caching. This page focuses on operator-owned browser and web-server controls rather than secure coding.

What this page includes

  • what HTTPS, HSTS, CORS, CSP, and adjacent headers actually defend;
  • where to configure them in Apache and Nginx;
  • deployable reference snippets for header and preflight handling;
  • what tools like Skipfish and w3af usually complain about, and how to interpret those findings.

Quick mental model

Think in four layers:

  1. transport - HTTPS and HSTS reduce downgrade and interception risk;
  2. browser exposure - CORS, CSP, frame restrictions, MIME controls, and referrer policy shape what the browser may read, execute, or embed;
  3. caching and delivery - Cache-Control, content type, and attachment behavior change how sensitive or active content behaves in user agents;
  4. review and verification - scanners, browser DevTools, curl, and route-by-route testing prove that what the team intended is what production actually emits.

What each control is for

HTTPS

Use HTTPS everywhere for authenticated pages, APIs, admin routes, upload/download flows, and supporting assets. The practical reason is not only secrecy of credentials in transit. Without HTTPS, an active network attacker can also alter returned HTML, CSS, JavaScript, or redirects and turn a delivery path into a code-execution path.

HSTS

HSTS tells a browser to keep using HTTPS for future visits to the host. This is stronger than โ€œwe redirect HTTP to HTTPSโ€ because a plain redirect still leaves the first insecure HTTP request exposed. HSTS only takes effect when learned over HTTPS, and preload is operationally useful only after the team is sure every covered host is truly HTTPS-only.

CORS

CORS is not authorization. It is a browser-enforced read/exposure policy for cross-origin JavaScript access. Treat it as a browser trust boundary, not as a substitute for server-side authn or authz.

CSP

CSP helps narrow what script, frame, and resource sources the browser may use. It is a containment and hardening mechanism for XSS, third-party script sprawl, and framing mistakes. It should be deployed deliberately, not as a giant wildcard copied from a forum answer.

Other headers worth owning

A strong baseline usually also includes:

  • X-Content-Type-Options: nosniff;
  • Referrer-Policy;
  • Permissions-Policy;
  • frame protections such as frame-ancestors in CSP, with X-Frame-Options kept only for older-client compatibility;
  • careful Cache-Control on personalized and export-like responses;
  • Cross-Origin-Resource-Policy or related cross-origin isolation headers only where the application model supports them.

What Skipfish and w3af findings usually mean

These tools often flag issues such as:

  • missing Strict-Transport-Security;
  • missing Content-Security-Policy;
  • missing X-Content-Type-Options;
  • permissive or inconsistent CORS responses;
  • weak clickjacking protections;
  • mixed or inconsistent cache rules on sensitive pages.

Treat those findings as configuration review prompts, not automatic severity truth. For example:

  • missing HSTS on a public marketing page is different from missing HSTS on an authenticated product origin;
  • a missing CSP on a purely static docs site is not the same risk as missing CSP on a privileged admin application;
  • missing nosniff on script or style routes is usually more important than on inert binary downloads.

Where to configure in Nginx

Common locations are:

  • /etc/nginx/nginx.conf for global behavior;
  • /etc/nginx/conf.d/*.conf for shared site fragments;
  • /etc/nginx/sites-available/*.conf and sites-enabled/ on Debian-style layouts;
  • the relevant server {} block for host-specific behavior;
  • location {} only when a route family genuinely needs a different policy.

Nginx baseline for headers on an authenticated web app

server {
    listen 443 ssl http2;
    server_name app.example.com;

    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-$request_id'; connect-src 'self' https://api.example.com" always;

    location / {
        proxy_pass http://app_upstream;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
    }
}

Nginx pattern for CORS preflight on a narrow API surface

location /api/public/ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin "https://frontend.example.com" always;
        add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
        add_header Access-Control-Allow-Credentials "true" always;
        add_header Access-Control-Max-Age "600" always;
        add_header Content-Length 0;
        add_header Content-Type text/plain;
        return 204;
    }

    add_header Access-Control-Allow-Origin "https://frontend.example.com" always;
    add_header Access-Control-Allow-Credentials "true" always;
    proxy_pass http://api_upstream;
}

Nginx review notes

  • keep exact origins for credentialed flows;
  • do not combine Access-Control-Allow-Credentials: true with *;
  • use always so headers still appear on relevant error paths;
  • prefer host- or route-specific CORS blocks rather than one site-wide wildcard rule.

Where to configure in Apache HTTP Server

Common locations are:

  • /etc/apache2/sites-available/*.conf on Debian-style systems;
  • /etc/httpd/conf/httpd.conf or /etc/httpd/conf.d/*.conf on RHEL-style systems;
  • the relevant <VirtualHost *:443> block for host-specific policy;
  • mod_headers, mod_ssl, and sometimes mod_rewrite for the actual controls.

Make sure these modules are available where needed:

sudo a2enmod ssl headers rewrite
sudo systemctl reload apache2

Apache baseline for HTTPS and headers

<VirtualHost *:443>
    ServerName app.example.com
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/app.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/app.example.com/privkey.pem

    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
    Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'"

    ProxyPass / http://127.0.0.1:8080/
    ProxyPassReverse / http://127.0.0.1:8080/
</VirtualHost>

Apache HTTPS redirect on the cleartext vhost

<VirtualHost *:80>
    ServerName app.example.com
    RewriteEngine On
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>

Apache CORS example for a narrow API path

<Location "/api/public/">
    Header always set Access-Control-Allow-Origin "https://frontend.example.com"
    Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
    Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
    Header always set Access-Control-Allow-Credentials "true"
    Header always set Access-Control-Max-Age "600"
</Location>

CORS interview and review prompts worth keeping

These questions are useful because they test whether someone understands browser security rather than memorized header names:

  1. What three things define an origin?
  2. Does CORS block the request, the response, or both in all cases?
  3. Why is Access-Control-Allow-Origin: * incompatible with credentialed browser flows?
  4. Which requests cause a preflight and why?
  5. Why is CORS not a defense against CSRF by itself?
  6. Why should OPTIONS handling often be optimized at the web-server or proxy layer instead of waking the whole app?

Route-by-route test commands

curl -I https://app.example.com/
curl -I https://app.example.com/login
curl -I https://app.example.com/account
curl -I https://app.example.com/admin
curl -I https://app.example.com/download/export.csv
curl -i -X OPTIONS   -H 'Origin: https://frontend.example.com'   -H 'Access-Control-Request-Method: POST'   -H 'Access-Control-Request-Headers: Authorization, Content-Type'   https://api.example.com/api/public/report

Production mistakes that keep recurring

  • HSTS enabled on one host but missing on login or admin subdomains;
  • CSP copied from a blog with broad wildcards and permanent unsafe-inline;
  • CORS set globally even though only a few routes need browser cross-origin access;
  • OPTIONS not implemented for real browser behavior, even though API docs look fine;
  • nosniff missing on script or style paths behind CDN or legacy static handlers;
  • cleartext port 80 left enabled without clean redirect and host coverage;
  • personalized pages cached too broadly by edge, proxy, or browser.

Use this page with


Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.