Securing REST APIs beyond authentication
Most breaches in real APIs are not broken crypto. They are CORS misconfiguration, mass assignment and a server that fetches a URL you gave it.
CORS, correctly
// CORS is a browser control, not an authorisation control.
const ALLOWED = new Set([
"https://app.example.com",
"https://admin.example.com",
]);
app.use(function cors(req, res, next) {
const origin = req.headers.origin;
if (origin && ALLOWED.has(origin)) {
res.set("Access-Control-Allow-Origin", origin); // echo the exact origin
res.set("Vary", "Origin"); // or caches will mix them up
res.set("Access-Control-Allow-Credentials", "true");
res.set("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE");
res.set("Access-Control-Allow-Headers", "Content-Type,Authorization,Idempotency-Key");
res.set("Access-Control-Max-Age", "600");
}
if (req.method === "OPTIONS") return res.status(204).end();
return next();
});- Never reflect an arbitrary Origin while also sending
Allow-Credentials: true— that combination lets any site read authenticated responses. - A wildcard origin with credentials is rejected by browsers anyway, so a working configuration is always an explicit list.
- Always send
Vary: Originso a shared cache does not serve one site's CORS headers to another. - CORS protects the browser's users, not your API. A curl client ignores it entirely, so authorisation must be server side.
Input handling and the classic injection risks
| Risk | How it happens | Control |
|---|---|---|
| Mass assignment | Binding the request body straight onto a model | An explicit allowlist of writable fields per operation |
| BOLA / IDOR | Reading an object by id without an ownership check | Scope every query by the caller's tenant or subject |
| SSRF | The server fetches a URL from the request body | An allowlist of hosts and schemes, no redirects, no link-local ranges |
| Injection | String-concatenated queries or shell commands | Parameterised queries and argument arrays |
| Excessive data exposure | Returning the whole entity | An explicit response schema, never the internal model |
| Log leakage | Logging full request bodies | Redact tokens, passwords, card data and personal fields |
| NoSQL injection | Passing an operator object from JSON into a query | Reject non-scalar values where a scalar is expected |
from flask import request, jsonify
import ipaddress, socket
from urllib.parse import urlparse
WRITABLE = {"name", "email", "locale"} # the allowlist, not a blocklist
def update_profile(user, body: dict):
# Mass assignment defence: only copy what the caller may write
changes = {k: v for k, v in body.items() if k in WRITABLE}
if not changes:
raise ValueError("no writable fields supplied")
if "email" in changes and not looks_like_email(changes["email"]):
raise ValueError("invalid email")
user.update(changes)
ALLOWED_HOSTS = {"cdn.partner.example", "images.partner.example"}
def fetch_partner_image(raw_url: str) -> bytes:
u = urlparse(raw_url)
if u.scheme not in ("https",):
raise ValueError("https only")
if u.hostname not in ALLOWED_HOSTS:
raise ValueError("host not allowed")
# Resolve first and reject anything that is not a public address
for family, _, _, _, sockaddr in socket.getaddrinfo(u.hostname, 443):
ip = ipaddress.ip_address(sockaddr[0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
raise ValueError("address not routable")
return http_get(raw_url, allow_redirects=False, timeout=5)⚠️
An owner check hidden inside a serializer, a decorator or an ORM default is not a control — it is a convention. Put the authorisation decision in the query that loads the object, so that a forgotten check returns no data rather than someone else's data.
FAQ
Is an API key enough to protect an internal API?
Only if the network also constrains who can reach it. Treat the key as one factor and add mTLS or a network policy, because keys leak through logs, repositories and screenshots.
Should I return 403 or 404 when a caller cannot see an object?
Return 404 when revealing existence is itself sensitive, and 403 when the caller legitimately knows the object exists. Be consistent within a resource, and never alternate in a way that leaks.
Related
Authentication and authorisation Handling uploads and binary payloads
Last refreshed 2026-09-18.