What is CORS?

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls which websites can make API calls to other domains.

What is an Origin?

Origin = Protocol + Domain + Port

https://example.com:443
  ↑       ↑         ↑
Protocol Domain   Port

Same-Origin Examples:

Origin 1: https://example.com/page1
Origin 2: https://example.com/page2
→ Same origin ✓

Cross-Origin Examples:

Origin 1: https://example.com
Origin 2: https://api.example.com
→ Different domain = Cross-origin ✗

Origin 1: https://example.com
Origin 2: http://example.com
→ Different protocol = Cross-origin ✗

Origin 1: https://example.com:443
Origin 2: https://example.com:8080
→ Different port = Cross-origin ✗

Why CORS Exists

Without CORS:

Malicious site: evil.com
    ↓
JavaScript: fetch('https://bank.com/transfer?to=attacker&amount=1000')
    ↓
Browser sends request with your bank cookies
    ↓
Money stolen!

With CORS:

Malicious site: evil.com
    ↓
JavaScript: fetch('https://bank.com/transfer')
    ↓
Browser checks: "Does bank.com allow evil.com?"
    ↓
No CORS headers from bank.com
    ↓
Browser blocks request ✓

CORS Request Headers (Browser → Server)

1. Origin

Automatically sent by browser on cross-origin requests

Origin: https://mysite.com

Meaning: Tells server which origin is making the request.

Browser adds this automatically - JavaScript cannot modify it.


2. Access-Control-Request-Method

Sent in OPTIONS preflight only

Access-Control-Request-Method: POST

Meaning: Tells server which HTTP method the actual request will use.


3. Access-Control-Request-Headers

Sent in OPTIONS preflight only

Access-Control-Request-Headers: authorization, content-type

Meaning: Tells server which custom headers the actual request will include.


CORS Response Headers (Server → Browser)

1. Access-Control-Allow-Origin

Required for all CORS responses

Access-Control-Allow-Origin: https://trusted-site.com

Meaning: Only trusted-site.com can read this response from JavaScript.

Wildcard (dangerous):

Access-Control-Allow-Origin: *

Meaning: Any website can read this response.

Example:

Request from: https://console.aws.amazon.com
API response includes:
Access-Control-Allow-Origin: https://console.aws.amazon.com

Browser: "Origin matches, allow!" ✓

2. Access-Control-Allow-Methods

Required in OPTIONS preflight response

Access-Control-Allow-Methods: GET, POST, PUT, DELETE

Meaning: Browser can use these HTTP methods in cross-origin requests.


3. Access-Control-Allow-Headers

Required in OPTIONS preflight response if custom headers used

Access-Control-Allow-Headers: Authorization, Content-Type, X-Custom-Header

Meaning: Browser can send these headers in cross-origin requests.

Example:

fetch('https://api.example.com', {
  headers: {
    'Authorization': 'Bearer token123',  // Needs Allow-Headers
    'X-Custom-Header': 'value'           // Needs Allow-Headers
  }
})

4. Access-Control-Expose-Headers

Optional - controls which response headers JavaScript can read

Access-Control-Expose-Headers: X-Request-ID, X-Rate-Limit

Meaning: JavaScript can access these headers from the response.

Default readable headers (no Expose-Headers needed):

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

Without this header:

fetch('https://api.example.com')
  .then(response => {
    console.log(response.headers.get('X-Request-ID')); // null
  });

With this header:

fetch('https://api.example.com')
  .then(response => {
    console.log(response.headers.get('X-Request-ID')); // "abc123"
  });

5. Access-Control-Max-Age

Optional - caches preflight response

Access-Control-Max-Age: 86400

Meaning: Browser can cache preflight result for 86400 seconds (24 hours).


6. Access-Control-Allow-Credentials

Required if sending cookies/credentials

Access-Control-Allow-Credentials: true

Meaning: Browser can send cookies and authentication headers.

JavaScript side:

fetch('https://api.example.com', {
  credentials: 'include'  // Send cookies
})

Cannot use with wildcard:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
→ Invalid! Must specify exact origin

Two Types of CORS Requests

1. Simple Requests (No Preflight)

Conditions:

  • Method: GET, HEAD, or POST
  • Headers: Only simple headers (Accept, Content-Type, etc.)
  • Content-Type: application/x-www-form-urlencoded, multipart/form-data, or text/plain

Flow:

Browser sends request directly
    ↓
GET https://api.example.com/data
    ↓
Server responds with CORS headers
← Access-Control-Allow-Origin: https://mysite.com
    ↓
Browser checks origin
    ↓
If matches: JavaScript can read response ✓
If doesn't match: Browser blocks response ✗

No OPTIONS request needed!


2. Preflighted Requests (Requires OPTIONS)

Conditions (any of these):

  • Method: PUT, DELETE, PATCH, CONNECT, TRACE
  • Custom headers: Authorization, X-Custom-Header, etc.
  • Content-Type: application/json

Flow:

1. Browser sends OPTIONS (preflight)
→ OPTIONS https://api.example.com/data
→ Access-Control-Request-Method: POST
→ Access-Control-Request-Headers: authorization,content-type

2. Server responds to OPTIONS
← HTTP/1.1 200 OK
← Access-Control-Allow-Origin: https://mysite.com
← Access-Control-Allow-Methods: GET, POST, PUT
← Access-Control-Allow-Headers: authorization,content-type
← Access-Control-Max-Age: 86400

3. Browser checks preflight response
If allowed: Continue to step 4
If not allowed: Block request ✗

4. Browser sends actual request
→ POST https://api.example.com/data
→ Authorization: Bearer token123
→ Content-Type: application/json

5. Server responds
← HTTP/1.1 200 OK
← Access-Control-Allow-Origin: https://mysite.com
← {"result": "success"}

6. Browser checks origin again
If matches: JavaScript can read response ✓

CORS Violation Patterns

Pattern 1: Missing Allow-Origin

Request: fetch('https://api.example.com')
Response: HTTP/1.1 200 OK (no CORS headers)

Browser Console:
Access to fetch at 'https://api.example.com' from origin 'https://mysite.com' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is 
present on the requested resource.

Pattern 2: Origin Mismatch

Request from: https://mysite.com
Response: Access-Control-Allow-Origin: https://othersite.com

Browser Console:
Access to fetch at 'https://api.example.com' from origin 'https://mysite.com' 
has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has 
a value 'https://othersite.com' that is not equal to the supplied origin.

Pattern 3: Preflight Failure

OPTIONS request fails (403, 404, 500)

Browser Console:
Access to fetch at 'https://api.example.com' from origin 'https://mysite.com' 
has been blocked by CORS policy: Response to preflight request doesn't pass 
access control check: It does not have HTTP ok status.

Pattern 4: Missing Allow-Headers

Request with: Authorization header
Response: Access-Control-Allow-Headers: content-type (missing authorization)

Browser Console:
Access to fetch at 'https://api.example.com' from origin 'https://mysite.com' 
has been blocked by CORS policy: Request header field authorization is not 
allowed by Access-Control-Allow-Headers in preflight response.

Pattern 5: Response Visible in HAR but Blocked by JavaScript

Critical scenario: Even when server sends response successfully, browser blocks JavaScript access.

1. OPTIONS preflight succeeds
→ OPTIONS https://api.example.com/data
← HTTP/1.1 200 OK
← Access-Control-Allow-Origin: https://mysite.com
← Access-Control-Allow-Methods: GET, POST

2. Browser sends GET request
→ GET https://api.example.com/data

3. Server responds successfully
← HTTP/1.1 200 OK
← Content-Type: application/json
← {"data": "hello world"}

4. Browser receives response (visible in Network tab/HAR file)

5. Browser checks CORS headers
   Missing: Access-Control-Allow-Origin

6. Browser blocks JavaScript access
   ✓ Response exists in browser memory
   ✗ JavaScript cannot read it

7. JavaScript sees error
fetch('https://api.example.com/data')
  .then(r => r.json())
  .catch(e => console.error(e))

// Error: CORS policy: No 'Access-Control-Allow-Origin' header

What you see:

Network tab (F12 → Network):

GET /data HTTP/1.1
Status: 200 OK
Response body: {"data": "hello world"}  ← Visible here

HAR file:

{
  "response": {
    "status": 200,
    "content": {
      "text": "{\"data\": \"hello world\"}"   Visible here
    }
  }
}

JavaScript console:

CORS error: No 'Access-Control-Allow-Origin' header
Cannot read response

Key concept: Browser enforces CORS at the application layer, not network layer.

  • Network layer: Response received successfully (200 OK)
  • Application layer: JavaScript blocked from reading it (CORS violation)

Both preflight AND actual response need CORS headers:

OPTIONS response must have:
- Access-Control-Allow-Origin
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers

GET/POST response must also have:
- Access-Control-Allow-Origin  ← Often forgotten!

CORS with Status 200

Important: CORS violations can happen even when HTTP returns 200 OK.

1. Browser sends request
2. Server responds: HTTP/1.1 200 OK (no CORS headers)
3. Browser receives response successfully
4. Browser checks CORS policy
5. Missing Access-Control-Allow-Origin
6. Browser blocks JavaScript from reading response
7. JavaScript sees: Error (even though HTTP was 200)

Network layer: Success (200 OK) Browser security layer: Blocked (CORS violation)


Real-World Example: AWS Console

Scenario:

AWS Console (https://console.aws.amazon.com) calls AWS API (https://api.aws.amazon.com)

Without CORS headers:

Console JavaScript:
fetch('https://api.aws.amazon.com/data')

Browser: "Cross-origin request!"
Browser: "No Access-Control-Allow-Origin header"
Browser: "BLOCKED"

Console: Error - Cannot access API

With CORS headers:

API Response:
Access-Control-Allow-Origin: https://console.aws.amazon.com
Access-Control-Allow-Headers: Authorization, X-Amz-Date
Access-Control-Expose-Headers: X-Amzn-Requestid

Browser: "Origin matches, headers allowed"
Browser: "Allow access"

Console: Success - Can read response ✓

Corporate Proxy and CORS

Problem: Proxy strips CORS headers

Browser → Proxy → AWS API
        ← 200 OK ← (with CORS headers)
        
Proxy strips headers:
- Removes Access-Control-Allow-Origin
- Removes Access-Control-Allow-Headers

Browser ← 200 OK (no CORS headers)

Browser: "CORS violation - blocked!"

Result: Status 200 but ERR_FAILED in browser.


Debugging CORS Issues

1. Check Browser Console

F12 → Console tab
Look for: "blocked by CORS policy"

2. Check Network Tab

F12 → Network tab → Click request
Headers tab:
- Check Response Headers for Access-Control-*
- Check if OPTIONS request exists
- Check OPTIONS response status

3. Test with curl (Bypasses CORS)

curl -v https://api.example.com

# If curl succeeds but browser fails
# → CORS issue confirmed

4. Check Preflight

curl -X OPTIONS https://api.example.com \
  -H "Origin: https://mysite.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization"

# Should return:
# Access-Control-Allow-Origin: https://mysite.com
# Access-Control-Allow-Methods: POST
# Access-Control-Allow-Headers: authorization

Technical Terms

  • CORS: Cross-Origin Resource Sharing - browser security mechanism
  • Origin: Protocol + Domain + Port combination
  • Same-Origin Policy: Browser blocks cross-origin requests by default
  • Preflight request: OPTIONS request sent before actual request
  • Simple request: Request that doesn’t require preflight
  • Preflighted request: Request that requires OPTIONS preflight
  • Access-Control-Allow-Origin: Header specifying allowed origins
  • Access-Control-Allow-Headers: Header specifying allowed request headers
  • Access-Control-Expose-Headers: Header specifying readable response headers
  • Access-Control-Max-Age: How long to cache preflight response

Common Mistakes

1. Wildcard with Credentials

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

→ Invalid! Cannot use * with credentials

2. Multiple Origins

Access-Control-Allow-Origin: https://site1.com, https://site2.com

→ Invalid! Only one origin allowed
→ Solution: Server checks origin and returns matching one

3. Missing Preflight Headers

Server handles POST but not OPTIONS

→ Preflight fails
→ Actual POST never sent

Notes

  • CORS is enforced by browsers, not servers
  • curl and server-to-server requests bypass CORS
  • CORS headers must be in every response, not just OPTIONS
  • Preflight responses can be cached (Access-Control-Max-Age)
  • Status 200 doesn’t mean CORS succeeded - check headers
  • Corporate proxies can strip CORS headers, causing failures
  • Always check browser console for CORS error messages