Security

CORS

Cross-Origin Resource Sharing - A security mechanism that allows web applications to make requests to a different domain than the one serving the web page, controlled through HTTP headers.

What is CORS?

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls whether web pages can make HTTP requests to domains different from the one serving the page. CORS implements the same-origin policy, a critical security model preventing malicious websites from making unauthorized requests to legitimate services using your browser’s stored credentials. Without CORS, any website could access your banking API, read your emails, or perform actions on social media using your logged-in session—a severe security vulnerability.

The same-origin policy compares protocol, domain, and port when determining whether requests cross origins. HTTPS and HTTP represent different protocols even for identical domains. Subdomains like api.example.com differ from example.com. Non-standard ports like :8080 create distinct origins from default ports. Browsers block cross-origin requests by default unless explicit CORS headers permit the specific origin, methods, and headers involved in the communication.

Understanding Same-Origin Policy

Origins consist of three components: protocol (https://), domain (example.com), and port (:443 implicit for HTTPS). Two URLs share the same origin when all three components match exactly. This strictness protects users—a malicious site at evil.com cannot read responses from bank.com even though both use HTTPS because their domains differ. The same-origin policy blocks JavaScript from accessing cross-origin responses preventing sensitive data leakage.

The policy blocks cross-origin reads but permits some writes. Forms can submit to cross-origin URLs because browsers don’t expose response contents to JavaScript. Images, scripts, and stylesheets load cross-origin because browsers limit their capabilities—images can’t read pixel data, scripts execute in isolated contexts. Full XHR and Fetch API requests require explicit CORS permission through response headers because JavaScript receives complete access to response data including sensitive information.

// Same-origin policy examples
interface OriginCheck {
  currentOrigin: string;
  targetUrl: string;
  allowed: boolean;
  reason: string;
}

const originChecks: OriginCheck[] = [
  {
    currentOrigin: 'https://example.com',
    targetUrl: 'https://example.com/api',
    allowed: true,
    reason: 'Same protocol, domain, and port'
  },
  {
    currentOrigin: 'https://example.com',
    targetUrl: 'https://api.example.com',
    allowed: false,
    reason: 'Different subdomain (api. prefix)'
  },
  {
    currentOrigin: 'https://example.com',
    targetUrl: 'http://example.com',
    allowed: false,
    reason: 'Different protocol (https vs http)'
  },
  {
    currentOrigin: 'https://example.com',
    targetUrl: 'https://example.com:8080',
    allowed: false,
    reason: 'Different port (443 vs 8080)'
  }
];

// Function to check if request crosses origins
function isCrossOrigin(currentOrigin: string, targetUrl: string): boolean {
  const current = new URL(currentOrigin);
  const target = new URL(targetUrl);

  return current.protocol !== target.protocol ||
         current.hostname !== target.hostname ||
         current.port !== target.port;
}

CORS Headers Explained

Access-Control-Allow-Origin specifies which origins may access resources. The wildcard ”*” allows any origin but prevents credential inclusion. Specific origins like “https://app.example.com” permit that exact origin with credential support. The header appears in responses telling browsers whether to allow JavaScript access to response data. Missing this header blocks cross-origin requests entirely regardless of other CORS headers present.

Access-Control-Allow-Methods lists permitted HTTP methods for cross-origin requests. Simple methods (GET, HEAD, POST with specific content types) don’t require preflight checks. Custom methods (PUT, DELETE, PATCH) trigger preflight OPTIONS requests where servers must declare method support. This header appears in both preflight responses and actual request responses ensuring method permission throughout the request lifecycle.

Access-Control-Allow-Headers declares which custom request headers cross-origin requests may include. Standard headers like Accept, Accept-Language, and Content-Type (for certain values) don’t require explicit permission. Custom headers like Authorization, X-API-Key, or application-specific headers must appear in this header or browsers reject the request. Servers must list all custom headers clients will send preventing unauthorized header injection attacks.

// CORS header configuration for different scenarios
interface CORSConfig {
  origin: string | string[] | '*';
  methods: string[];
  allowedHeaders: string[];
  exposedHeaders?: string[];
  credentials?: boolean;
  maxAge?: number;
}

function configureCORS(req: Request, config: CORSConfig): Headers {
  const headers = new Headers();

  // Handle origin - specific origin vs wildcard
  if (Array.isArray(config.origin)) {
    const requestOrigin = req.headers.get('origin');
    if (requestOrigin && config.origin.includes(requestOrigin)) {
      headers.set('Access-Control-Allow-Origin', requestOrigin);
    }
  } else {
    headers.set('Access-Control-Allow-Origin', config.origin);
  }

  // Set allowed methods
  headers.set('Access-Control-Allow-Methods', config.methods.join(', '));

  // Set allowed headers
  headers.set('Access-Control-Allow-Headers', config.allowedHeaders.join(', '));

  // Expose headers if specified
  if (config.exposedHeaders) {
    headers.set('Access-Control-Expose-Headers', config.exposedHeaders.join(', '));
  }

  // Enable credentials if specified (requires specific origin)
  if (config.credentials) {
    headers.set('Access-Control-Allow-Credentials', 'true');
  }

  // Set preflight cache duration
  if (config.maxAge) {
    headers.set('Access-Control-Max-Age', config.maxAge.toString());
  }

  return headers;
}

// Public API configuration
const publicAPIConfig: CORSConfig = {
  origin: '*',
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type']
};

// Authenticated API configuration
const authAPIConfig: CORSConfig = {
  origin: ['https://app.example.com', 'https://dashboard.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Total-Count', 'X-Page-Number'],
  credentials: true,
  maxAge: 3600
};

CORS Preflight Requests

Complex requests trigger preflight OPTIONS requests before actual requests execute. Complexity arises from non-simple methods (PUT, DELETE, PATCH), custom headers (Authorization, X-API-Key), or certain Content-Type values (application/json). Browsers send OPTIONS requests asking servers whether the actual request is allowed. Servers respond with Access-Control-* headers granting or denying permission.

Access-Control-Max-Age caches preflight responses avoiding redundant OPTIONS requests. Setting max-age to 3600 (1 hour) or 86400 (24 hours) reduces preflight overhead significantly for repeated requests to the same endpoint. Browsers respect this cache duration eliminating preflight requests for subsequent matching requests. However, changing request headers or methods invalidates the cache requiring new preflight checks.

Preflight failures prevent actual requests from executing. If the preflight response lacks required CORS headers or denies the requested method/headers, browsers block the actual request without ever sending it to the server. This protection prevents unauthorized cross-origin requests while informing legitimate clients about permission requirements through preflight responses.

// Handling CORS preflight and actual requests
async function handleCORSRequest(request: Request): Promise<Response> {
  const origin = request.headers.get('origin');

  // Allowed origins (in production, use environment variables)
  const allowedOrigins = [
    'https://app.example.com',
    'https://dashboard.example.com',
    'http://localhost:3000' // Development only
  ];

  const isAllowed = origin && allowedOrigins.includes(origin);

  // Handle preflight OPTIONS request
  if (request.method === 'OPTIONS') {
    if (!isAllowed) {
      return new Response(null, { status: 403 });
    }

    return new Response(null, {
      status: 204,
      headers: {
        'Access-Control-Allow-Origin': origin,
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key',
        'Access-Control-Max-Age': '86400', // 24 hours
        'Access-Control-Allow-Credentials': 'true'
      }
    });
  }

  // Handle actual request
  if (!isAllowed) {
    return new Response('Origin not allowed', { status: 403 });
  }

  // Process request and create response
  const data = { message: 'Success', timestamp: Date.now() };

  return new Response(JSON.stringify(data), {
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': origin,
      'Access-Control-Allow-Credentials': 'true',
      'Access-Control-Expose-Headers': 'X-Request-ID',
      'X-Request-ID': crypto.randomUUID()
    }
  });
}

Implementing CORS on Servers

Server-side CORS implementation varies by framework but follows consistent patterns. Examine the Origin header from requests, determine whether to allow the origin, set appropriate Access-Control-* headers in responses. Wildcard origins (”*”) simplify public APIs but prevent credential support. Specific origins enable cookies and authentication headers but require maintaining allowed origin lists or implementing origin validation logic.

Dynamic origin validation enables flexible CORS policies without hardcoding origin lists. Examine origin headers against patterns (regex matching subdomains), database lookups (checking registered API consumers), or environment-based configuration (different origins for production/staging/development). Callback functions receive origins and return boolean permission decisions enabling sophisticated policies based on runtime conditions.

Credential support requires careful configuration. Access-Control-Allow-Credentials: true combined with specific origins enables cookies, Authorization headers, and other credentials in cross-origin requests. This powerful capability demands strict origin validation—allowing credentials from untrusted origins enables malicious sites to make authenticated requests on behalf of users. Never combine credentials: true with origin: ”*” as browsers reject this insecure combination.

// Production-ready CORS middleware for various scenarios
class CORSMiddleware {
  private allowedOrigins: Set<string>;
  private allowedMethods: string[];
  private allowedHeaders: string[];
  private exposedHeaders: string[];
  private maxAge: number;

  constructor(config: {
    origins: string[];
    methods?: string[];
    headers?: string[];
    exposedHeaders?: string[];
    maxAge?: number;
  }) {
    this.allowedOrigins = new Set(config.origins);
    this.allowedMethods = config.methods || ['GET', 'POST', 'PUT', 'DELETE'];
    this.allowedHeaders = config.headers || ['Content-Type', 'Authorization'];
    this.exposedHeaders = config.exposedHeaders || [];
    this.maxAge = config.maxAge || 86400;
  }

  isOriginAllowed(origin: string | null): boolean {
    if (!origin) return false;
    return this.allowedOrigins.has(origin) || this.allowedOrigins.has('*');
  }

  async handle(request: Request, next: () => Promise<Response>): Promise<Response> {
    const origin = request.headers.get('origin');

    // Handle preflight
    if (request.method === 'OPTIONS') {
      if (!this.isOriginAllowed(origin)) {
        return new Response('Origin not allowed', { status: 403 });
      }

      return new Response(null, {
        status: 204,
        headers: this.buildCORSHeaders(origin!)
      });
    }

    // Process actual request
    const response = await next();

    // Add CORS headers to response
    if (this.isOriginAllowed(origin)) {
      const corsHeaders = this.buildCORSHeaders(origin!);
      corsHeaders.forEach((value, key) => {
        response.headers.set(key, value);
      });
    }

    return response;
  }

  private buildCORSHeaders(origin: string): Headers {
    const headers = new Headers();

    headers.set('Access-Control-Allow-Origin', origin);
    headers.set('Access-Control-Allow-Methods', this.allowedMethods.join(', '));
    headers.set('Access-Control-Allow-Headers', this.allowedHeaders.join(', '));

    if (this.exposedHeaders.length > 0) {
      headers.set('Access-Control-Expose-Headers', this.exposedHeaders.join(', '));
    }

    headers.set('Access-Control-Allow-Credentials', 'true');
    headers.set('Access-Control-Max-Age', this.maxAge.toString());

    return headers;
  }
}

// Usage with different configurations
const publicAPI = new CORSMiddleware({
  origins: ['*'],
  methods: ['GET'],
  headers: ['Content-Type']
});

const protectedAPI = new CORSMiddleware({
  origins: [
    'https://app.example.com',
    'https://dashboard.example.com'
  ],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  headers: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Total-Count', 'X-Request-ID'],
  maxAge: 3600
});

Bypassing CORS Restrictions

CORS restrictions apply only to browsers—server-to-server requests, cURL commands, and API clients don’t enforce CORS. Browsers implement CORS protecting users from malicious cross-origin requests. Development proxies bypass CORS by running on the same origin as applications, forwarding requests to cross-origin APIs server-side where CORS doesn’t apply. This pattern works perfectly for development but requires different production solutions.

CorsProxy provides production-ready CORS bypass eliminating the need for backend proxy implementations or browser extensions. Applications make requests to CorsProxy URLs specifying target destinations through parameters. CorsProxy fetches content server-side where CORS doesn’t apply, adds appropriate CORS headers, and returns responses to browsers. This approach works transparently with existing client code requiring only URL modifications without architectural changes.

The simplest way to fix CORS errors is to prepend your API URL with CorsProxy:

// Fix CORS errors by using CorsProxy
// Original URL that causes CORS error:
const blockedUrl = 'https://api.example.com/data';

// Fixed URL using CorsProxy:
const corsProxyUrl = `https://corsproxy.io/?url=${encodeURIComponent('https://api.example.com/data')}`;

// Make the request - no CORS errors!
const response = await fetch(corsProxyUrl);
const data = await response.json();

// With authentication and proxy options:
const authenticatedUrl = `https://corsproxy.io/?url=${encodeURIComponent('https://api.example.com/data')}&key=your-api-key&type=residential&colo=fra`;

Browser extensions that disable CORS exist but should never be used in production or shared with users. These extensions fundamentally break browser security exposing users to attacks CORS prevents. Development-only usage with extreme caution represents the only acceptable application. Production solutions must respect CORS using proper server configuration, proxy services, or architectural patterns that avoid cross-origin requests.

// Using CorsProxy to bypass CORS restrictions
class CORSProxyClient {
  private apiKey: string;
  private proxyType: 'residential' | 'datacenter';
  private colo: string;

  constructor(apiKey: string, options: { proxyType?: 'residential' | 'datacenter', colo?: string } = {}) {
    this.apiKey = apiKey;
    this.proxyType = options.proxyType || 'residential';
    this.colo = options.colo || 'fra';
  }

  async fetch(url: string, options: RequestInit = {}): Promise<Response> {
    const proxyUrl = `https://corsproxy.io/?url=${encodeURIComponent(url)}&key=${this.apiKey}&type=${this.proxyType}&colo=${this.colo}`;

    return fetch(proxyUrl, {
      ...options,
      headers: {
        ...options.headers,
        'Accept': 'application/json'
      }
    });
  }

  async get(url: string): Promise<any> {
    const response = await this.fetch(url);
    return response.json();
  }

  async post(url: string, data: any): Promise<any> {
    const response = await this.fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    });
    return response.json();
  }
}

// Usage - No CORS errors even with cross-origin requests
const client = new CORSProxyClient('your-api-key');

// Fetch from any origin without CORS issues
const githubData = await client.get('https://api.github.com/users/octocat');
const apiData = await client.post('https://api.example.com/data', {
  key: 'value'
});

console.log('Data fetched successfully:', githubData, apiData);

CORS Security Considerations

Never allow credentials with wildcard origins. The combination Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true is invalid—browsers reject it preventing the security vulnerability it would create. If credentials are needed, specify exact allowed origins validating them carefully against whitelists or secure pattern matching.

Validate origins server-side rather than trusting request headers alone. While Origin headers generally contain legitimate values, server-side validation ensures only approved origins receive CORS access. Implement origin checking in security-critical middleware examining origins against configured lists, database entries, or validated patterns before setting CORS headers.

Limit exposed headers to necessary values avoiding leaking sensitive information through Access-Control-Expose-Headers. By default, browsers expose only simple response headers (Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma) to cross-origin JavaScript. Custom headers like X-Total-Count, X-Request-ID, or pagination metadata require explicit exposure. Never expose authentication tokens, internal system identifiers, or sensitive debugging information through exposed headers.

Best Practices for CORS

Implement CORS as early as possible in request processing pipelines before authentication or business logic executes. Failed CORS checks should reject requests immediately without consuming resources on forbidden operations. Preflight requests especially should return quickly with minimal processing avoiding expensive authentication or database queries.

Use appropriate cache durations for preflight responses balancing performance and flexibility. Shorter durations (600-3600 seconds) suit APIs under active development where CORS policies change frequently. Longer durations (86400 seconds) benefit stable production APIs reducing preflight overhead for repeat clients. Monitor preflight request rates adjusting max-age values to optimize the tradeoff between cache effectiveness and policy update propagation.

Document CORS requirements clearly for API consumers including allowed origins, required headers, supported methods, and credential policies. Provide example requests showing proper CORS header usage and expected responses. Clear documentation reduces support burden and prevents integration issues stemming from CORS misunderstandings.

Learn More

Create a free Account to fix CORS Errors in Production

Say goodbye to CORS errors and get back to building great web applications. It's free!

CORSPROXY Dashboard

Related Terms

More in Security

Related guides

Back to Glossary