Skip to content

Security

QuoinAPI ships several middleware layers that cover the most common production security hardening steps out of the box. All of them are configurable via QUOIN_* environment variables and safe to run in development with their default values.


CORS Hardening

Cross-Origin Resource Sharing is controlled by CORSMiddleware, configured in app/core/middlewares.py.

Configuration

Variable Default Notes
QUOIN_BACKEND_CORS_ORIGINS ["http://localhost:3000", "http://localhost:8000"] Empty list disables CORS entirely
QUOIN_BACKEND_CORS_ALLOW_METHODS ["GET","POST","PUT","PATCH","DELETE","OPTIONS"]
QUOIN_BACKEND_CORS_ALLOW_HEADERS ["Authorization","Content-Type","X-Request-ID"]
QUOIN_BACKEND_CORS_ALLOW_CREDENTIALS true See warning below

Wildcard footgun protection

Browsers silently refuse credentialed CORS responses when the server responds with Access-Control-Allow-Methods: * or Access-Control-Allow-Headers: *. QuoinAPI detects this at startup and raises a RuntimeError if you combine wildcards with allow_credentials=True outside development:

RuntimeError: CORS misconfiguration: allow_credentials=True with wildcard
allow_methods/allow_headers is rejected outside development.

This is intentional — a silent browser refusal is harder to debug than a startup crash.

In development the guard is skipped so you can use loose settings during local work.

Production example

# .env.production
QUOIN_BACKEND_CORS_ORIGINS=["https://app.example.com"]
QUOIN_BACKEND_CORS_ALLOW_METHODS=["GET","POST","PUT","DELETE","OPTIONS"]
QUOIN_BACKEND_CORS_ALLOW_HEADERS=["Authorization","Content-Type"]
QUOIN_BACKEND_CORS_ALLOW_CREDENTIALS=true

Security Headers

SecurityHeadersMiddleware adds a standard set of defensive response headers. It is enabled by default and runs on every HTTP response.

Toggle via QUOIN_SECURITY_HEADERS_ENABLED=false if your reverse proxy (NGINX, Caddy, Cloudflare) manages headers instead.

Headers emitted

Header Default value Purpose
X-Content-Type-Options nosniff Prevents MIME-type sniffing
X-Frame-Options DENY Blocks framing / clickjacking
Referrer-Policy strict-origin-when-cross-origin Limits referrer leakage
Permissions-Policy geolocation=(), camera=(), microphone=() Disables unused browser APIs
Content-Security-Policy See below Restricts resource loading
Strict-Transport-Security max-age=31536000; includeSubDomains Forces HTTPS (browsers only honour over HTTPS)

Content-Security-Policy

The default CSP accommodates the built-in homepage (Google Fonts, simpleicons CDN, and inline styles/scripts):

default-src 'self';
style-src  'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net;
font-src   'self' https://fonts.gstatic.com;
img-src    'self' https://cdn.simpleicons.org https://fastapi.tiangolo.com;
script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
frame-ancestors 'none';
base-uri 'self'

The default covers two built-in UIs:

  • Homepage — Google Fonts (fonts.googleapis.com / fonts.gstatic.com) and tech-logo icons (cdn.simpleicons.org).
  • Swagger UI (/docs) — FastAPI loads its UI assets and favicon from cdn.jsdelivr.net and fastapi.tiangolo.com.

unsafe-inline

Both style-src and script-src include 'unsafe-inline' because the homepage uses inline <style> and <script> blocks. Moving those to static files would let you drop 'unsafe-inline' for a stricter policy.

Override it for your own frontend:

QUOIN_SECURITY_CSP=default-src 'self'; img-src 'self' data:; \
  frame-ancestors 'none'; base-uri 'self'

HSTS tuning

HSTS is emitted by default. Browsers only honour it over HTTPS — over HTTP it is silently ignored. Set max-age=0 to suppress the header entirely (e.g. behind a TLS-terminating proxy that sets it itself):

QUOIN_SECURITY_HSTS_MAX_AGE=0

Enable the preload directive only once your domain is submitted to the HSTS preload list — it is hard to reverse:

QUOIN_SECURITY_HSTS_PRELOAD=true

Request Size Limit

RequestSizeLimitMiddleware rejects requests whose Content-Length exceeds the configured cap before the route handler reads the body. It returns a 413 Content Too Large RFC 9457 Problem Details response:

{
  "type": "urn:quoin:error:payload_too_large",
  "title": "Content Too Large",
  "status": 413,
  "detail": "Request body exceeds 1048576 bytes",
  "instance": "/api/v1/users"
}
Variable Default Notes
QUOIN_MAX_REQUEST_BODY_BYTES 1048576 (1 MiB) <=0 disables the cap

Tuning for file uploads

If a route accepts file uploads, raise the cap or disable it for that deployment:

# Allow up to 10 MiB globally
QUOIN_MAX_REQUEST_BODY_BYTES=10485760

Chunked transfers

The middleware only checks the advertised Content-Length. Conforming HTTP clients always send it. The underlying uvicorn/h11 layer caps raw protocol buffers for the rare chunked case.


Middleware ordering

Middleware is registered in LIFO order via add_middleware, so the execution order from outermost to innermost is:

TimeoutMiddleware          ← outermost: wraps full lifecycle
RequestSizeLimitMiddleware ← reject oversize before downstream reads
RequestIDMiddleware
SecurityHeadersMiddleware
TrustedHostMiddleware
CORSMiddleware             ← innermost

See Also