In the previous tutorial, you learned how CSRF attacks trick browsers into making unwanted requests. CORS is the browser’s mechanism for controlling which websites can make requests to your server. These two concepts are closely related, and developers often confuse them.

What Is the Same-Origin Policy?

Before we talk about CORS, you need to understand the same-origin policy. This is the browser’s most important security rule.

Two URLs have the same origin if they share the same protocol, host, and port:

URL AURL BSame Origin?Reason
https://api.example.comhttps://api.example.com/usersYesSame protocol, host, port
https://example.comhttp://example.comNoDifferent protocol
https://example.comhttps://api.example.comNoDifferent host (subdomain)
https://example.comhttps://example.com:8080NoDifferent port

By default, browsers block JavaScript from reading responses from a different origin. Your frontend at https://app.example.com cannot read data from https://api.example.com unless the API explicitly allows it.

Important: The browser sends the request. The server receives it and responds. But the browser blocks JavaScript from reading the response if the origins do not match. The request still reaches the server.

What Is CORS?

CORS (Cross-Origin Resource Sharing) is a mechanism that lets servers tell browsers: “This other origin is allowed to read my responses.”

The server does this by including specific HTTP headers in its response:

Access-Control-Allow-Origin: https://app.example.com

When the browser sees this header, it allows JavaScript on app.example.com to read the response.

Simple Requests vs Preflight Requests

Browsers handle CORS differently depending on the request type.

Simple Requests

A request is “simple” if it meets ALL of these conditions:

  • Method is GET, HEAD, or POST
  • Content-Type is application/x-www-form-urlencoded, multipart/form-data, or text/plain
  • No custom headers

For simple requests, the browser sends the request immediately and checks the CORS headers in the response.

Preflight Requests

If the request does not meet the “simple” criteria (for example, it uses PUT/DELETE, has a JSON content type, or includes custom headers), the browser sends a preflight request first.

A preflight is an OPTIONS request that asks: “Is this cross-origin request allowed?”

# Preflight request (sent automatically by the browser)
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

The server responds with what is allowed:

# Preflight response
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

If the preflight succeeds, the browser sends the actual request. If the preflight fails, the browser does not send the actual cross-origin request — even though the OPTIONS request itself did reach your server.

CORS Headers Explained

HeaderPurposeExample
Access-Control-Allow-OriginWhich origins can access the resourcehttps://app.example.com or *
Access-Control-Allow-MethodsWhich HTTP methods are allowedGET, POST, PUT, DELETE
Access-Control-Allow-HeadersWhich custom headers are allowedContent-Type, Authorization
Access-Control-Allow-CredentialsWhether cookies/auth headers are includedtrue or omit
Access-Control-Max-AgeHow long to cache preflight results (seconds)86400 (24 hours)
Access-Control-Expose-HeadersWhich response headers JS can readX-Total-Count

The Danger of Access-Control-Allow-Origin: *

The wildcard * means “any origin can read this response.” This is fine for public APIs that serve open data. But it is dangerous if your API uses cookies or returns private data.

Critical rule: You cannot use * together with Access-Control-Allow-Credentials: true. The browser will reject it. If your API uses cookies, you must specify the exact origin.

# This is INVALID — browser will reject it
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

# This is CORRECT
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

Configuring CORS in Go

Using the rs/cors Middleware

package main

import (
    "encoding/json"
    "net/http"

    "github.com/rs/cors"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/users", handleUsers)

    // Configure CORS
    c := cors.New(cors.Options{
        AllowedOrigins:   []string{"https://app.example.com"},
        AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
        AllowedHeaders:   []string{"Content-Type", "Authorization"},
        AllowCredentials: true,
        MaxAge:           86400, // Cache preflight for 24 hours
    })

    handler := c.Handler(mux)
    http.ListenAndServe(":8080", handler)
}

func handleUsers(w http.ResponseWriter, r *http.Request) {
    users := []map[string]string{
        {"name": "Alex", "email": "alex@example.com"},
    }
    json.NewEncoder(w).Encode(users)
}

Manual CORS in Go (Without a Library)

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        origin := r.Header.Get("Origin")

        // Only allow specific origins
        allowedOrigins := map[string]bool{
            "https://app.example.com": true,
            "https://admin.example.com": true,
        }

        if allowedOrigins[origin] {
            w.Header().Set("Access-Control-Allow-Origin", origin)
            w.Header().Set("Vary", "Origin") // Important for caching proxies
            w.Header().Set("Access-Control-Allow-Credentials", "true")
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
            w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
            w.Header().Set("Access-Control-Max-Age", "86400")
        }

        // Handle preflight
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusNoContent)
            return
        }

        next.ServeHTTP(w, r)
    })
}

Configuring CORS in Python

Django (django-cors-headers)

# Install: pip install django-cors-headers

# settings.py
INSTALLED_APPS = [
    # ...
    "corsheaders",
]

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",  # Must be before CommonMiddleware
    "django.middleware.common.CommonMiddleware",
    # ...
]

# Allow specific origins
CORS_ALLOWED_ORIGINS = [
    "https://app.example.com",
    "https://admin.example.com",
]

# Allow credentials (cookies, Authorization header)
CORS_ALLOW_CREDENTIALS = True

# Allowed headers
CORS_ALLOW_HEADERS = [
    "content-type",
    "authorization",
    "x-csrf-token",
]

Flask (flask-cors)

# Install: pip install flask-cors
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)

# Configure CORS for specific routes
CORS(app, resources={
    r"/api/*": {
        "origins": ["https://app.example.com"],
        "methods": ["GET", "POST", "PUT", "DELETE"],
        "allow_headers": ["Content-Type", "Authorization"],
        "supports_credentials": True,
        "max_age": 86400,
    }
})

@app.route("/api/users")
def get_users():
    return [{"name": "Alex", "email": "alex@example.com"}]

Configuring CORS in JavaScript (Express)

import express from "express";
import cors from "cors";

const app = express();

// Configure CORS
const corsOptions = {
  origin: ["https://app.example.com", "https://admin.example.com"],
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true, // Allow cookies
  maxAge: 86400, // Cache preflight for 24 hours
};

app.use(cors(corsOptions));

app.get("/api/users", (req, res) => {
  res.json([{ name: "Alex", email: "alex@example.com" }]);
});

app.listen(3000);

Dynamic Origin Validation

If you need to allow multiple origins dynamically:

const corsOptions = {
  origin: (origin, callback) => {
    const allowedOrigins = [
      "https://app.example.com",
      "https://admin.example.com",
    ];

    // Allow requests with no origin (mobile apps, server-to-server)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error("Not allowed by CORS"));
    }
  },
  credentials: true,
};

app.use(cors(corsOptions));

CORS vs CSRF: What Is the Difference?

Developers often confuse CORS and CSRF. They are related but different:

CORSCSRF
What it isBrowser policy for reading cross-origin responsesAttack that tricks browser into making unwanted requests
Who enforces itBrowserYour server (with tokens, headers, cookies)
What it preventsUnauthorized data readingUnauthorized state changes
Applies toJavaScript (fetch, XMLHttpRequest)Forms, image tags, any browser request

Key insight: CORS does not prevent CSRF. A form submission does not trigger CORS — the browser sends it directly. CORS only applies to JavaScript-initiated requests.

A malicious site can still submit a form to your server. The browser will include cookies. CORS will not stop this. You need CSRF tokens and SameSite cookies for that.

Common CORS Mistakes

Mistake 1: Reflecting the Origin Header

// DANGEROUS — reflects any origin
origin := r.Header.Get("Origin")
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")

This effectively allows every website to read your API responses with credentials. An attacker’s site can steal user data.

Fix: Validate the origin against an allowlist.

Mistake 2: Using Wildcard With Credentials

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Browsers reject this combination. But some developers “fix” it by reflecting the origin (Mistake 1), which is worse.

Fix: Specify exact allowed origins when using credentials.

Mistake 3: Exposing Internal APIs

Access-Control-Allow-Origin: *

If your internal admin API has this header, any website on the internet can query it from a user’s browser — inside your corporate network.

Fix: Internal APIs should not have CORS headers at all, or should use strict origin allowlists.

Mistake 4: Not Handling Preflight

If your server does not respond to OPTIONS requests, every non-simple cross-origin request will fail. This is a common cause of “CORS error” messages in the browser console.

Fix: Handle OPTIONS requests in your CORS middleware or configuration.

CORS for Development vs Production

During local development, your frontend runs on localhost:3000 and your API runs on localhost:8080. They are different origins (different ports).

// Development CORS configuration
if os.Getenv("ENV") == "development" {
    c = cors.New(cors.Options{
        AllowedOrigins:   []string{"http://localhost:3000"},
        AllowCredentials: true,
        AllowedHeaders:   []string{"Content-Type", "Authorization"},
    })
} else {
    c = cors.New(cors.Options{
        AllowedOrigins:   []string{"https://app.example.com"},
        AllowCredentials: true,
        AllowedHeaders:   []string{"Content-Type", "Authorization"},
    })
}

Never use AllowedOrigins: ["*"] in production with credentials.

Proxy Approach: Avoiding CORS Entirely

Some teams avoid CORS by serving the frontend and API from the same origin using a reverse proxy (Nginx, Caddy):

server {
    listen 443 ssl;
    server_name example.com;

    # Frontend
    location / {
        proxy_pass http://frontend:3000;
    }

    # API — same origin, no CORS needed
    location /api/ {
        proxy_pass http://backend:8080;
    }
}

Both the frontend and API are served from example.com. No cross-origin requests, no CORS headers needed.

Prevention Checklist

RulePriorityNotes
Never use * with credentialsHighBrowsers reject it, devs work around it unsafely
Validate origins against allowlistHighNever reflect the Origin header blindly
Handle OPTIONS preflight requestsHighRequired for non-simple requests
Set Access-Control-Max-AgeMediumReduces preflight requests (improves performance)
Use a reverse proxy for same-originMediumEliminates CORS entirely
Separate dev/production CORS configMediumLoose in dev, strict in production
Do not expose internal APIs via CORSHighInternal APIs should not be accessible from browsers

What is Next?

In the next tutorial, you will learn about API security — rate limiting, input validation, and API key management. APIs are the most attacked surface in modern applications, and proper security goes beyond just CORS.