Browser security boundaries
Configure CORS narrowly
Allow only the browser origins, methods, headers, and credential behavior an API actually needs.
CORS tells browsers which other origins may read a response. That is all it does. It is not authentication, and it does nothing to protect non-browser clients like curl or a mobile app, which ignore these headers entirely.
The dangerous pattern is reflecting whatever Origin the request carried and allowing credentials. That combination lets any site read a signed-in user’s data.
// Vulnerable: echoes the caller's origin and allows cookies
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', req.headers.origin)
res.set('Access-Control-Allow-Credentials', 'true')
next()
})
Allow one origin at a time
The safe version keeps an explicit allowlist and answers with a single, exact origin. When you vary the header by caller, you must also send Vary: Origin so caches do not serve one origin’s response to another.
const allowed = new Set([
'https://app.flaviocopes.com',
'http://localhost:3000',
])
app.use((req, res, next) => {
const origin = req.headers.origin
if (allowed.has(origin)) {
res.set('Access-Control-Allow-Origin', origin)
res.set('Access-Control-Allow-Credentials', 'true')
res.set('Vary', 'Origin')
}
next()
})
Never return Access-Control-Allow-Origin: * together with credentials. The browser refuses that combination, and reaching for it is usually a sign the allowlist is missing.
An API reflects any Origin and allows credentials. A page on an attacker-controlled origin can read the signed-in user’s private API response.
A broad wildcard feels convenient during development, but it can escape into production unnoticed. Keep local origins explicit and separate from the production allowlist.
Send preflight and credentialed requests from the production origin, the local origin, and an unlisted origin. Save the response headers and prove the unlisted page cannot read the protected response.
Lesson completed