Safeguard
DevSecOps

CORS in Node.js: What It Is and How to Configure It Securely

CORS in Node.js trips up almost every developer at some point. Here is what CORS actually does, why you need it, and how to configure it without opening a hole.

Priya Mehta
Security Analyst
5 min read

CORS in Node.js is a browser-enforced permission system that decides whether a web page from one origin is allowed to read responses from your API on a different origin — and misconfiguring it is one of the most common ways developers accidentally weaken an otherwise fine backend. CORS stands for Cross-Origin Resource Sharing, and understanding it means understanding that it is not a feature of Node itself but a set of HTTP headers your Node server sends that the browser then enforces. Get the headers right and legitimate frontends work while malicious pages are blocked. Get them wrong and you either break your own app or hand read access to any site on the internet.

What is CORS in Node.js, really

By default, browsers enforce the same-origin policy: JavaScript running on https://app.example.com cannot read a response from https://api.example.com because the origins differ (different subdomain counts). CORS is the controlled exception. When your Node.js API includes the right Access-Control-Allow-Origin header, the browser relaxes the same-origin rule for that specific origin.

The key mental model: CORS is enforced by the browser, not your server. A curl request or a server-to-server call ignores CORS entirely. So CORS is not an authentication or authorization mechanism — it only governs what browser-based JavaScript from other origins may read. Treating it as a security boundary for anything beyond the browser is a mistake.

Why we use CORS in Node.js

You reach for CORS the moment your frontend and backend live on different origins, which is nearly always: a React app on one domain talking to a Node API on another, a local dev server on localhost:3000 calling an API on localhost:8080, or a mobile web app hitting a shared API. Without the right headers the browser blocks the response and you see the familiar console error about a missing Access-Control-Allow-Origin. CORS is what lets those legitimate cross-origin calls through in a controlled way.

The preflight request

For anything beyond a simple GET or basic POST — custom headers, a JSON content type, methods like PUT or DELETE — the browser first sends an OPTIONS preflight request asking whether the real request is allowed. Your Node server must answer that preflight with the allowed methods and headers, or the browser never sends the actual request. Missing preflight handling is behind a large share of "it works in Postman but not the browser" bug reports.

Configuring node cors with Express

The cors npm package is the standard middleware. The insecure-but-common starting point is to allow everything:

const express = require('express');
const cors = require('cors');
const app = express();

// DON'T ship this: allows every origin
app.use(cors());

A bare app.use(cors()) reflects a wildcard origin, which means any website can make browser requests and read the responses. For a public, read-only, credential-free API that might be acceptable, but for anything that returns user data it is a liability.

The secure pattern is an explicit allowlist:

const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com',
];

app.use(cors({
  origin(origin, callback) {
    // allow same-origin / server-to-server (no Origin header)
    if (!origin || allowedOrigins.includes(origin)) {
      return callback(null, true);
    }
    return callback(new Error('Not allowed by CORS'));
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
}));

The dangerous combination to avoid

There is one configuration that is actively unsafe: reflecting the request's origin back and setting credentials: true. If you echo whatever Origin the caller sends and also allow credentials, you have effectively allowed any site to make authenticated requests as your logged-in users and read the results. The CORS spec forbids combining a literal * origin with credentials for this reason, but developers sometimes reconstruct the same hole by dynamically reflecting the origin without an allowlist check.

The rule: if credentials: true, the origin must come from a fixed allowlist, never from reflecting the incoming header unconditionally.

// Unsafe: reflects any origin AND allows credentials
app.use(cors({
  origin: (origin, cb) => cb(null, true), // reflects everything
  credentials: true,
}));

CORS is not your whole defense

Because CORS lives in the browser, it does nothing against direct HTTP clients. Real protection still comes from authentication on every endpoint, authorization checks per resource, input validation, and CSRF defenses for cookie-based sessions. CORS decides which browser origins may read your responses; it never decides who is allowed to call your API. A misconfigured cors js setup is worth catching in review, and the Safeguard Academy has material on API security patterns that pair with getting CORS right. If your API is one piece of a larger service, our DAST product exercises running endpoints for exactly these kinds of header and access-control issues.

FAQ

What is CORS in Node.js in one sentence?

It is a set of HTTP headers your Node server sends that tell the browser which other origins are allowed to read your API's responses, relaxing the browser's same-origin policy in a controlled way.

Why do we use CORS in Node.js?

Because your frontend and API usually run on different origins, and the browser blocks cross-origin reads by default. CORS headers grant a specific, named frontend permission to make those calls.

Is app.use(cors()) safe?

Not for APIs that return sensitive data. A bare call allows a wildcard origin, meaning any website can read the responses. Use an explicit origin allowlist instead.

Does CORS protect my API from attackers?

No. CORS is enforced only by browsers, so a direct HTTP client bypasses it entirely. It is not authentication or authorization. You still need auth checks, input validation, and CSRF protection.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.