Web Security Headersμ CSP
Web Security Headersμ CSP¶
μ΄μ : 08. Injection 곡격과 λ°©μ΄ | λ€μ: 10_API_Security.md
HTTP 보μ ν€λλ μΉ μ ν리μΌμ΄μ μ 첫 λ²μ§Έ λ°©μ΄μ μ λλ€. λΈλΌμ°μ κ° λ³΄μ μ μ± μ μ μ©νλλ‘ μ§μνμ¬ ν¬λ‘μ€ μ¬μ΄νΈ μ€ν¬λ¦½ν κ³Ό ν΄λ¦μ¬νΉλΆν° νλ‘ν μ½ λ€μ΄κ·Έλ μ΄λ 곡격 λ° λ°μ΄ν° μ μΆκΉμ§ μ 체 곡격 μ νμ μνν©λλ€. λ¨μΌ ν€λκ° λλ½λλ©΄ μ μμ±λ μ ν리μΌμ΄μ λ μ·¨μ½ν΄μ§ μ μμ΅λλ€. μ΄ λ μ¨μ Flaskμ Djangoμ μ€μ©μ μΈ κ΅¬μ± μμ μ ν¨κ» λͺ¨λ μ£Όμ 보μ ν€λμ λν ν¬κ΄μ μΈ κ°μ΄λλ₯Ό μ 곡ν©λλ€.
νμ΅ λͺ©ν¶
- Content-Security-Policy (CSP)μ λͺ©μ κ³Ό μ§μλ¬Έ μ΄ν΄
- HTTPS μ°κ²°μ κ°μ νλ HSTS ꡬμ±
- X-Content-Type-Options, X-Frame-Options, Referrer-Policy ν€λ μ μ©
- λΈλΌμ°μ κΈ°λ₯μ μ ννλ Permissions-Policy ꡬν
- Cross-Origin μ μ± (CORP, COEP, COOP) ꡬμ±
- Subresource Integrity (SRI)λ₯Ό μ¬μ©νμ¬ μΈλΆ 리μμ€ νμΈ
- Flask λ° Django μ ν리μΌμ΄μ μμ 보μ ν€λ μ€μ
- λͺ λ Ήμ€ λꡬ λ° μ€μΊλλ₯Ό μ¬μ©νμ¬ ν€λ ν μ€νΈ λ° κ°μ¬
1. 보μ ν€λ κ°μ¶
1.1 보μ ν€λκ° μ€μν μ΄μ ¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HTTP Response 보μ ν€λ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Server ββββ HTTP Response βββββΆ Browser β
β β β
β βββ Content-Security-Policy β
β βββ Strict-Transport-Security β
β βββ X-Content-Type-Options β
β βββ X-Frame-Options β
β βββ Referrer-Policy β
β βββ Permissions-Policy β
β βββ Cross-Origin-Resource-Policy β
β βββ Cross-Origin-Embedder-Policy β
β βββ Cross-Origin-Opener-Policy β
β β
β μ΄ ν€λλ€μ λΈλΌμ°μ μ λ€μμ μ§μν©λλ€: β
β β’ μΈλΌμΈ μ€ν¬λ¦½νΈ λ° λ¬΄λ¨ λ¦¬μμ€ μ°¨λ¨ (CSP) β
β β’ νμ HTTPS μ¬μ© (HSTS) β
β β’ MIME νμ
μ€λν λ°©μ§ (X-Content-Type-Options) β
β β’ νλ μ΄λ° / ν΄λ¦μ¬νΉ μ°¨λ¨ (X-Frame-Options) β
β β’ Referrer μ 보 μ μΆ μ μ΄ (Referrer-Policy) β
β β’ λΈλΌμ°μ API μ κ·Ό μ ν (Permissions-Policy) β
β β’ Cross-origin 리μμ€ κ²©λ¦¬ (CORP/COEP/COOP) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1.2 μ¬μΈ΅ λ°©μ΄¶
보μ ν€λλ μμ ν μ½λ© κ΄νμ λ체νλ κ²μ΄ μλλΌ μΆκ° κ³μΈ΅μ λλ€. μλ²½νκ² μ½λ©λ μ ν리μΌμ΄μ μ΄λΌλ 보μ ν€λλ λ€μμ λν 보νΈλ₯Ό μ 곡ν©λλ€:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β μ¬μΈ΅ λ°©μ΄ κ³μΈ΅ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Layer 5: 보μ ν€λ (λΈλΌμ°μ κ°μ μ μ±
) β
β Layer 4: μ ν리μΌμ΄μ
λ‘μ§ (μ
λ ₯ κ²μ¦, μΈμ¦) β
β Layer 3: νλ μμν¬ λ³΄νΈ (CSRF ν ν°, ORM) β
β Layer 2: λ€νΈμν¬ λ³΄μ (TLS, λ°©νλ²½, WAF) β
β Layer 1: μΈνλΌ (OS κ°ν, ν¨μΉ) β
β β
β κ° κ³μΈ΅μ νμ κ³μΈ΅μ΄ λμΉ μ μλ κ²μ ν¬μ°©ν©λλ€. β
β 보μ ν€λλ μ ν리μΌμ΄μ
μ½λκ° λμΉ μ μλ κ²μ ν¬μ°©ν©λλ€. β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1.3 λΉ λ₯Έ μ°Έμ‘° ν μ΄λΈ¶
| ν€λ | μν λμ | μΌλ°μ μΈ κ° |
|---|---|---|
| Content-Security-Policy | XSS, λ°μ΄ν° μ£Όμ | default-src 'self' |
| Strict-Transport-Security | νλ‘ν μ½ λ€μ΄κ·Έλ μ΄λ, μΏ ν€ κ°λ‘μ±κΈ° | max-age=31536000; includeSubDomains |
| X-Content-Type-Options | MIME νμ νΌλ | nosniff |
| X-Frame-Options | ν΄λ¦μ¬νΉ | DENY |
| Referrer-Policy | μ 보 μ μΆ | strict-origin-when-cross-origin |
| Permissions-Policy | λ¬΄λ¨ API μ κ·Ό | camera=(), microphone=() |
| Cross-Origin-Resource-Policy | Cross-origin λ°μ΄ν° μ μΆ | same-origin |
| Cross-Origin-Embedder-Policy | Spectre μ€νμΌ κ³΅κ²© | require-corp |
| Cross-Origin-Opener-Policy | Cross-window 곡격 | same-origin |
2. Content-Security-Policy (CSP)¶
2.1 CSPλ 무μμΈκ°?¶
Content-Security-Policyλ κ°μ₯ κ°λ ₯ν 보μ ν€λμ λλ€. λΈλΌμ°μ κ° μ λ’°ν΄μΌ νλ μ½ν μΈ μμ€μ νμ© λͺ©λ‘μ μ μνμ¬ λ¬΄λ¨ μ€ν¬λ¦½νΈ μ€νμ μ°¨λ¨ν¨μΌλ‘μ¨ XSS 곡격μ ν¨κ³Όμ μΌλ‘ λ°©μ§ν©λλ€.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CSP κ°μ λͺ¨λΈ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β λΈλΌμ°μ κ° CSP ν€λλ₯Ό μμ : β
β Content-Security-Policy: default-src 'self'; script-src 'self' β
β β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β <script src= β β <script> β β
β β "/app.js"> β β alert('XSS') β β
β β β β </script> β β
β β μΆμ²: self β β μΆμ²: inline β β
β β β νμ© β β β μ°¨λ¨ β β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β <script src= β β <img src= β β
β β "https://cdn β β "/logo.png"> β β
β β .example.com β β β β
β β /lib.js"> β β μΆμ²: self β β
β β μΆμ²: cdn β β β νμ© β β
β β β μ°¨λ¨ β β β β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2.2 CSP μ§μλ¬Έ¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CSP μ§μλ¬Έ μΉ΄ν
κ³ λ¦¬ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Fetch μ§μλ¬Έ (리μμ€ λ‘λ μμΉ μ μ΄): β
β βββ default-src λ€λ₯Έ λͺ¨λ fetch μ§μλ¬Έμ λ체 β
β βββ script-src JavaScript μμ€ β
β βββ style-src CSS μμ€ β
β βββ img-src μ΄λ―Έμ§ μμ€ β
β βββ font-src μΉ ν°νΈ μμ€ β
β βββ connect-src XHR, fetch, WebSocket, EventSource β
β βββ media-src μ€λμ€/λΉλμ€ μμ€ β
β βββ object-src <object>, <embed>, <applet> β
β βββ child-src μΉ μ컀 λ° μ€μ²© 컨ν
μ€νΈ β
β βββ worker-src Worker, SharedWorker, ServiceWorker β
β βββ manifest-src μ± λ§€λνμ€νΈ β
β β
β λ¬Έμ μ§μλ¬Έ: β
β βββ base-uri <base> μμ μ ν β
β βββ sandbox μλλ°μ€ μ ν μ μ© β
β βββ plugin-types νλ¬κ·ΈμΈ MIME νμ
μ ν (deprecated) β
β β
β νμ μ§μλ¬Έ: β
β βββ form-action νΌ μ μΆ λμ μ ν β
β βββ frame-ancestors μ΄ νμ΄μ§λ₯Ό μλ² λν μ μλ λμ μ ν β
β βββ navigate-to νμ λμ μ ν (μ νμ μ§μ) β
β β
β λ³΄κ³ μ§μλ¬Έ: β
β βββ report-uri μλ° λ³΄κ³ μ μ‘ (deprecated) β
β βββ report-to μλ° λ³΄κ³ μ μ‘ (μ΅μ ) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2.3 μμ€ κ°¶
| μμ€ | μλ―Έ | μμ |
|---|---|---|
'self' |
λμΌ μΆμ² (μ€ν΄ + νΈμ€νΈ + ν¬νΈ) | script-src 'self' |
'none' |
λͺ¨λ μμ€ μ°¨λ¨ | object-src 'none' |
'unsafe-inline' |
μΈλΌμΈ μ€ν¬λ¦½νΈ/μ€νμΌ νμ© | style-src 'unsafe-inline' |
'unsafe-eval' |
eval(), Function() λ± νμ© | script-src 'unsafe-eval' |
'strict-dynamic' |
μ λ’°νλ μ€ν¬λ¦½νΈλ‘ λ‘λλ μ€ν¬λ¦½νΈ μ λ’° | script-src 'strict-dynamic' |
'nonce-<base64>' |
nonceλ‘ νΉμ μΈλΌμΈ νμ© | script-src 'nonce-abc123' |
'sha256-<hash>' |
ν΄μλ‘ νΉμ μΈλΌμΈ νμ© | script-src 'sha256-...' |
https: |
λͺ¨λ HTTPS μμ€ | img-src https: |
data: |
Data URI | img-src data: |
blob: |
Blob URI | worker-src blob: |
*.example.com |
μμΌλμΉ΄λ μλΈλλ©μΈ | script-src *.cdn.com |
2.4 CSP μ μ± λ¨κ³λ³ ꡬ좶
"""
CSP μ μ±
μ μ μ§μ μΌλ‘ κ΅¬μΆ β νμ©μ μΈ κ²λΆν° μ격ν κ²κΉμ§.
"""
# ββ λ 벨 1: κΈ°λ³Έ CSP (λͺ
λ°±ν XSS μ°¨λ¨) ββββββββββββββββββββββ
# default-src 'self'λ‘ μμνκ³ νμμ λ°λΌ μν
csp_level1 = "default-src 'self'"
# ββ λ 벨 2: νΉμ μΈλΆ 리μμ€ νμ© βββββββββββββββββββ
csp_level2 = (
"default-src 'self'; "
"script-src 'self' https://cdn.jsdelivr.net; "
"style-src 'self' https://fonts.googleapis.com; "
"font-src 'self' https://fonts.gstatic.com; "
"img-src 'self' data: https:; "
"connect-src 'self' https://api.example.com; "
"object-src 'none'; "
"base-uri 'self'; "
"form-action 'self'"
)
# ββ λ 벨 3: Nonce κΈ°λ° CSP (μ΅μ μ± κΆμ₯) ββββββ
# μλ²κ° μμ²λΉ λλ€ nonce μμ±
import secrets
def generate_csp_with_nonce():
nonce = secrets.token_urlsafe(32)
csp = (
f"default-src 'self'; "
f"script-src 'self' 'nonce-{nonce}'; "
f"style-src 'self' 'nonce-{nonce}'; "
f"img-src 'self' data:; "
f"font-src 'self'; "
f"connect-src 'self'; "
f"object-src 'none'; "
f"base-uri 'self'; "
f"form-action 'self'; "
f"frame-ancestors 'none'"
)
return csp, nonce
csp_header, nonce = generate_csp_with_nonce()
# HTMLμμ: <script nonce="<nonce>">...</script>
# ββ λ 벨 4: ν΄μ κΈ°λ° CSP (μ μ μΈλΌμΈ μ€ν¬λ¦½νΈμ©) βββββββββ
import hashlib
import base64
def compute_csp_hash(script_content: str) -> str:
"""CSP μΈλΌμΈ μ€ν¬λ¦½νΈ νμ© λͺ©λ‘μ μν SHA-256 ν΄μ κ³μ°."""
digest = hashlib.sha256(script_content.encode('utf-8')).digest()
b64 = base64.b64encode(digest).decode('utf-8')
return f"'sha256-{b64}'"
inline_script = "console.log('Hello, World!');"
script_hash = compute_csp_hash(inline_script)
print(f"CSP ν΄μ: {script_hash}")
# μΆλ ₯: CSP ν΄μ: 'sha256-TWupyvVdPa1DyFqLnQMqRpuUWdS3nKPnz70IcS/1o3Q='
csp_level4 = f"script-src 'self' {script_hash}"
# ββ λ 벨 5: strict-dynamic (μ€ν¬λ¦½νΈλ₯Ό λμ μΌλ‘ λ‘λνλ μ±μ©)
csp_level5 = (
"script-src 'strict-dynamic' 'nonce-{nonce}'; "
"object-src 'none'; "
"base-uri 'self'"
)
# strict-dynamicμ μ¬μ©νλ©΄ nonceκ° μλ μ€ν¬λ¦½νΈλ‘ λ‘λλ μ€ν¬λ¦½νΈλ
# μΆμ²μ κ΄κ³μμ΄ μλμΌλ‘ μ λ’°λ©λλ€.
2.5 CSP λ³΄κ³ ¶
"""
CSP μλ° λ³΄κ³ β μ°¨λ¨ μμ΄ μ μ±
μλ° κ°μ§.
"""
# ββ Report-Only λͺ¨λ (κ°μ μμ΄ λͺ¨λν°λ§) βββββββββββββββ
# λ¨Όμ Content-Security-Policy-Report-Only ν€λ μ¬μ©
csp_report_only = (
"default-src 'self'; "
"script-src 'self'; "
"report-uri /csp-report; "
"report-to csp-endpoint"
)
# Report-To ν€λ (report-to μ§μλ¬Έμ λλ°μ)
report_to = {
"group": "csp-endpoint",
"max_age": 86400,
"endpoints": [
{"url": "https://example.com/csp-report"}
]
}
# ββ CSP μλ° λ³΄κ³ λ₯Ό λ°μ Flask μλν¬μΈνΈ ββββββββββββββ
from flask import Flask, request, jsonify
import json
import logging
app = Flask(__name__)
logger = logging.getLogger('csp_reports')
@app.route('/csp-report', methods=['POST'])
def csp_report():
"""CSP μλ° λ³΄κ³ μμ λ° λ‘κΉ
."""
try:
# CSP λ³΄κ³ λ application/csp-reportλ‘ μ μ‘λ¨
report = request.get_json(force=True)
violation = report.get('csp-report', {})
logger.warning(
"CSP μλ°: blocked_uri=%s, "
"violated_directive=%s, "
"document_uri=%s, "
"source_file=%s, "
"line_number=%s",
violation.get('blocked-uri', 'N/A'),
violation.get('violated-directive', 'N/A'),
violation.get('document-uri', 'N/A'),
violation.get('source-file', 'N/A'),
violation.get('line-number', 'N/A'),
)
return jsonify({"status": "received"}), 204
except Exception as e:
logger.error(f"CSP λ³΄κ³ μ²λ¦¬ μ€λ₯: {e}")
return jsonify({"error": "invalid report"}), 400
# ββ CSP μλ° λ³΄κ³ JSON μμ ββββββββββββββββββββββββββββ
example_report = {
"csp-report": {
"document-uri": "https://example.com/page",
"referrer": "",
"violated-directive": "script-src 'self'",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self'",
"blocked-uri": "https://evil.com/malicious.js",
"status-code": 200,
"source-file": "https://example.com/page",
"line-number": 15,
"column-number": 2
}
}
2.6 μΌλ°μ μΈ CSP μ€μ¶
"""
μΌλ°μ μΈ CSP μ€μμ μμ λ°©λ².
"""
# ββ μ€μ 1: script-srcμ 'unsafe-inline' μ¬μ© βββββββββββββ
# μ΄κ²μ XSS λ°©μ§λ₯Ό μν CSPμ λͺ©μ μ 무ν¨νν¨
bad_csp = "script-src 'self' 'unsafe-inline'"
# μμ : λμ nonceλ ν΄μ μ¬μ©
good_csp = "script-src 'self' 'nonce-{random}'"
# ββ μ€μ 2: script-srcμ μμΌλμΉ΄λ ββββββββββββββββββββββββββββ
# λͺ¨λ μλΈλλ©μΈμμ μ€ν¬λ¦½νΈ λ‘λ νμ©
bad_csp = "script-src 'self' *.googleapis.com"
# μμ : νΉμ νΈμ€νΈλͺ
μ¬μ©
good_csp = "script-src 'self' https://ajax.googleapis.com"
# ββ μ€μ 3: default-src λλ½ βββββββββββββββββββββββββββββββ
# default-src μμΌλ©΄ λμ΄λμ§ μμ μ§μλ¬Έμ κΈ°λ³Έμ μΌλ‘ λͺ¨λ νμ©
bad_csp = "script-src 'self'"
# μμ : νμ λμ²΄λ‘ default-src ν¬ν¨
good_csp = "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'"
# ββ μ€μ 4: script-srcμμ data: νμ© ββββββββββββββββββββββ
# data: URIλ μ€ν κ°λ₯ν JavaScriptλ₯Ό ν¬ν¨ν μ μμ
bad_csp = "script-src 'self' data:"
# μμ : νμν κ³³μλ§ μ΄λ―Έμ§/ν°νΈμ data: μ¬μ©
good_csp = "script-src 'self'; img-src 'self' data:"
# ββ μ€μ 5: object-src μμ ββββββββββββββββββββββββββββ
# Flashμ Java μ νλ¦Ώμ μ€ν¬λ¦½νΈ μ€ν κ°λ₯
bad_csp = "default-src 'self'" # object-srcκ° 'self'λ‘ λ체λ¨
# μμ : object-srcλ₯Ό λͺ
μμ μΌλ‘ μ°¨λ¨
good_csp = "default-src 'self'; object-src 'none'"
# ββ μ€μ 6: μ§λμΉκ² νμ©μ μΈ connect-src ββββββββββββββββββββ
# λͺ¨λ HTTPS μλν¬μΈνΈλ‘ λ°μ΄ν° μ μΆ νμ©
bad_csp = "connect-src https:"
# μμ : νΉμ API μλν¬μΈνΈ λμ΄
good_csp = "connect-src 'self' https://api.example.com"
3. Strict-Transport-Security (HSTS)¶
3.1 HSTS μλ λ°©μ¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HSTS λ³΄νΈ νλ¦ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β HSTS μμ΄: β
β User ββhttp://βββΆ Server ββ301 RedirectβββΆ https:// β
β β β
β βββ MITMμ΄ μ΄ νλ¬Έ HTTP μμ²μ κ°λ‘μ± μ μμ β
β β
β HSTSμ ν¨κ»: β
β User ββhttp://βββΆ λΈλΌμ°μ κ° κ°λ‘μ± (307 λ΄λΆ 리λλ νΈ) β
β Browser ββhttps://βββΆ Server β
β (HTTPλ‘ λ€νΈμν¬ μμ² μμ) β
β β
β 첫 λ°©λ¬Έ: β
β 1. λΈλΌμ°μ κ° HTTPλ‘ μ°κ²° β
β 2. μλ²κ° 301 + HSTS ν€λλ‘ μλ΅ β
β 3. λΈλΌμ°μ κ° λλ©μΈμ λν HSTS μ μ±
μ μ₯ β
β β
β μ΄ν λ°©λ¬Έ: β
β 1. λΈλΌμ°μ κ° μλμΌλ‘ HTTPSλ‘ μ
κ·Έλ μ΄λ β
β 2. HTTP μμ²μ΄ λΈλΌμ°μ λ₯Ό λ λμ§ μμ β
β 3. μλͺ»λ μΈμ¦μλ νλ μ€ν¨ μ λ° (μ°ν μμ) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
3.2 HSTS μ§μλ¬Έ¶
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
| μ§μλ¬Έ | λͺ©μ | κΆμ₯ κ° |
|---|---|---|
max-age |
HSTSλ₯Ό κΈ°μ΅ν κΈ°κ° (μ΄) | 31536000 (1λ
) |
includeSubDomains |
λͺ¨λ μλΈλλ©μΈμ μ μ© | μμ ν 보νΈλ₯Ό μν΄ ν¬ν¨ |
preload |
λΈλΌμ°μ μ¬μ λ‘λ λͺ©λ‘μ ν¬ν¨ μμ² | ν μ€νΈ ν ν¬ν¨ |
"""
HSTS κ΅¬μ± κ³ λ € μ¬ν.
"""
# ββ λ°°ν¬ μ λ΅: μ μ§μ λ‘€μμ βββββββββββββββββββββββββ
# μ§§μ max-ageλ‘ μμνκ³ μκ°μ΄ μ§λ¨μ λ°λΌ μ¦κ°
# 1λ¨κ³: 5λΆμΌλ‘ ν
μ€νΈ
hsts_test = "max-age=300"
# 2λ¨κ³: 1μ£ΌμΌλ‘ μ¦κ°
hsts_week = "max-age=604800"
# 3λ¨κ³: μλΈλλ©μΈκ³Ό ν¨κ» 1κ°μλ‘ μ¦κ°
hsts_month = "max-age=2592000; includeSubDomains"
# 4λ¨κ³: μ 체 λ°°ν¬ (1λ
+ preload)
hsts_full = "max-age=31536000; includeSubDomains; preload"
# κ²½κ³ : λͺ¨λ μλΈλλ©μΈμ΄ HTTPSλ₯Ό μ§μνλμ§ νμΈνκΈ° μ μ
# κΈ΄ max-ageλ₯Ό μ€μ νλ©΄ μ¬μ©μλ₯Ό μ κΈ μ μμ΅λλ€.
# includeSubDomainsλ λͺ¨λ μλΈλλ©μΈμ΄ μ ν¨ν TLSλ₯Ό κ°μ ΈμΌ ν¨μ μλ―Έν©λλ€.
# ββ HSTS Preload λͺ©λ‘ βββββββββββββββββββββββββββββββββββββββββββ
# HSTS preload λͺ©λ‘μ λΈλΌμ°μ μ λ΄μ₯λμ΄ μμ΅λλ€.
# μ΄ λͺ©λ‘μ λλ©μΈμ 첫 λ°©λ¬Έμλ νμ HTTPSλ₯Ό μ¬μ©ν©λλ€.
# μ μΆ: https://hstspreload.org/
#
# Preload μꡬμ¬ν:
# 1. μ ν¨ν μΈμ¦μ
# 2. λμΌ νΈμ€νΈμμ λͺ¨λ HTTPλ₯Ό HTTPSλ‘ λ¦¬λλ νΈ
# 3. λ€μμ ν¬ν¨νλ HSTS ν€λ:
# - max-age >= 31536000 (1λ
)
# - includeSubDomains
# - preload
# 4. HTTPS 리λλ νΈλ HSTS ν€λλ₯Ό ν¬ν¨ν΄μΌ ν¨
4. X-Content-Type-Options¶
4.1 MIME μ€λν 곡격¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MIME μ€λν 곡격 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 곡격μκ° μ
λ‘λ: malicious.jpg β
β (μ€μ λ‘λ μ΄λ―Έμ§ λ°μ΄ν°κ° μλ JavaScript ν¬ν¨) β
β β
β μλ²κ° μ 곡: Content-Type: image/jpeg β
β β
β nosniff μμ΄: β
β λΈλΌμ°μ κ° μ½ν
μΈ μ€λν βββΆ "μ΄κ²μ JavaScriptμ²λΌ 보μ" β
β λΈλΌμ°μ κ° νμΌμ JavaScriptλ‘ μ€ν βββΆ XSS! β
β β
β nosniffμ ν¨κ»: β
β λΈλΌμ°μ κ° Content-Type μ λ’° βββΆ "μ΄κ²μ image/jpeg" β
β λΈλΌμ°μ κ° μ΄λ―Έμ§λ‘ λ λλ§ (μ€ν¨) βββΆ μ€ν¬λ¦½νΈ μ€ν μμ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
4.2 ꡬ챶
X-Content-Type-Options: nosniff
μ΄κ²μ κ°μ₯ κ°λ¨ν 보μ ν€λμ
λλ€ β μ νν νλμ μ ν¨ν κ° nosniffλ§ μμ΅λλ€. λͺ¨λ μλ΅μ ν¬ν¨νμ§ μμ μ΄μ κ° μμ΅λλ€. λ€μμ λ°©μ§ν©λλ€:
- μ€ν¬λ¦½νΈκ° μλ MIME νμ μ νμΌμμ μ€ν¬λ¦½νΈκ° λ‘λλλ κ²
- μ€νμΌμνΈκ° CSSκ° μλ MIME νμ μ νμΌμμ λ‘λλλ κ²
- μ¬μ©μ μ λ‘λ μ½ν μΈ λ₯Ό μ 곡ν λ MIME νμ νΌλ 곡격
5. X-Frame-Optionsμ frame-ancestors¶
5.1 ν΄λ¦μ¬νΉ λ°©μ§¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ν΄λ¦μ¬νΉ 곡격 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 곡격μμ νμ΄μ§: β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β "μ¬κΈ°λ₯Ό ν΄λ¦νλ©΄ μνμ λ°μΌμΈμ!" β β
β β β β
β β βββββββββββββββββββββββββββββββββββ β β
β β β 보μ΄μ§ μλ iframe (opacity: 0)β β β
β β β ββββββββββββββββββββββββββ β β β
β β β β κ·νμ μν μ± β β β β
β β β β β β β β
β β β β [1000μ μ‘κΈ] ββββββΌββββββΌββββ μ¬μ©μκ° μ¬κΈ°λ₯Ό ν΄λ¦ β
β β β β β β β β
β β β ββββββββββββββββββββββββββ β β β
β β βββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β μ¬μ©μλ μν λ²νΌμ ν΄λ¦νλ€κ³ μκ°νμ§λ§, β
β μ€μ λ‘λ μν μΉμ¬μ΄νΈκ° ν¬ν¨λ 보μ΄μ§ μλ iframeμ β
β "μ‘κΈ" λ²νΌμ ν΄λ¦νκ³ μμ΅λλ€. β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
5.2 X-Frame-Options κ°¶
# λͺ¨λ νλ μ΄λ° κ±°λΆ
X-Frame-Options: DENY
# λμΌ μΆμ²λ§ νμ©
X-Frame-Options: SAMEORIGIN
# νΉμ μΆμ² νμ© (deprecated, κΆμ₯νμ§ μμ)
X-Frame-Options: ALLOW-FROM https://trusted.example.com
5.3 CSP frame-ancestors (μ΅μ λ체)¶
"""
frame-ancestorsλ X-Frame-Optionsμ CSP λ체μ
λλ€.
λ μΈλ°ν μ μ΄λ₯Ό μ 곡νκ³ μ¬λ¬ μΆμ²λ₯Ό μ§μν©λλ€.
"""
# ββ λͺ¨λ νλ μ΄λ° μ°¨λ¨ (X-Frame-Options: DENYμ λλ±) βββββ
csp_no_frame = "frame-ancestors 'none'"
# ββ λμΌ μΆμ²λ§ νμ© βββββββββββββββββββββββββββββββββββββββ
csp_same_origin = "frame-ancestors 'self'"
# ββ νΉμ μΆμ² νμ© βββββββββββββββββββββββββββββββββββββββ
csp_specific = "frame-ancestors 'self' https://trusted.example.com https://partner.example.com"
# ββ X-Frame-Optionsμμ μ£Όμ μ°¨μ΄μ ββββββββββββββββββββββββ
#
# | κΈ°λ₯ | X-Frame-Options | frame-ancestors |
# |---------------------|----------------------|------------------------|
# | λ€μ€ μΆμ² | μλμ€ | μ |
# | μμΌλμΉ΄λ μλΈλλ©μΈ| μλμ€ | μ (*.example.com) |
# | μ€ν΄ μ ν | μλμ€ | μ (https:) |
# | CSP ν΅ν© | λ³λ ν€λ | CSPμ μΌλΆ |
# | λΈλΌμ°μ μ§μ | λ²μ© | μ΅μ λΈλΌμ°μ |
#
# κΆμ₯μ¬ν: μ΅λ νΈνμ±μ μν΄ λ λ€ μ€μ
# X-Frame-Options: DENY
# Content-Security-Policy: frame-ancestors 'none'
6. Referrer-Policy¶
6.1 Referrer μ 보 μ μΆ¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Referrer μ μΆ μλλ¦¬μ€ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β μ¬μ©μκ° λ€μμ μμ: https://bank.com/accounts/12345/transfer?amount=500 β
β β
β λ§ν¬λ₯Ό ν΄λ¦: https://analytics.example.com β
β β
β Referrer-Policy μμ΄: β
β Referer: https://bank.com/accounts/12345/transfer?amount=500 β
β β κ²½λ‘μ 쿼리 λ§€κ°λ³μλ₯Ό ν¬ν¨ν μ 체 URL μ μΆ! β
β β
β Referrer-Policy: strict-origin-when-cross-originκ³Ό ν¨κ» β
β Referer: https://bank.com β
β β μΆμ²λ§ μ μ‘, λ―Όκ°ν κ²½λ‘λ λ§€κ°λ³μ μμ β
β β
β Referrer-Policy: no-referrerμ ν¨κ» β
β Referer: (λΉμ΄ μμ) β
β β referrer μ λ³΄κ° μ ν μ μ‘λμ§ μμ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
6.2 μ μ± κ°¶
| μ μ± | λμΌ μΆμ² | Cross-Origin (HTTPSβHTTPS) | λ€μ΄κ·Έλ μ΄λ (HTTPSβHTTP) |
|---|---|---|---|
no-referrer |
μμ | μμ | μμ |
no-referrer-when-downgrade |
μ 체 URL | μ 체 URL | μμ |
origin |
μΆμ²λ§ | μΆμ²λ§ | μΆμ²λ§ |
origin-when-cross-origin |
μ 체 URL | μΆμ²λ§ | μΆμ²λ§ |
same-origin |
μ 체 URL | μμ | μμ |
strict-origin |
μΆμ²λ§ | μΆμ²λ§ | μμ |
strict-origin-when-cross-origin |
μ 체 URL | μΆμ²λ§ | μμ |
unsafe-url |
μ 체 URL | μ 체 URL | μ 체 URL |
"""
κΆμ₯ Referrer-Policy ꡬμ±.
"""
# ββ κΈ°λ³Έ κΆμ₯μ¬ν ββββββββββββββββββββββββββββββββββββββ
# strict-origin-when-cross-originμ μ΅μ λΈλΌμ°μ μ κΈ°λ³Έκ°μ΄λ©°
# κΈ°λ₯κ³Ό κ°μΈμ 보 보νΈμ μ’μ κ· νμ μ 곡ν©λλ€
referrer_policy = "strict-origin-when-cross-origin"
# ββ μ΅λ κ°μΈμ 보 λ³΄νΈ βββββββββββββββββββββββββββββββββββββββββ
# no-referrerλ λͺ¨λ referrer μ 보λ₯Ό μ κ±°ν©λλ€
# λ¨μ : λΆμ λ° μΌλΆ CSRF 보νΈλ₯Ό μμμν΅λλ€
referrer_max_privacy = "no-referrer"
# ββ λ―Όκ°ν URLμ΄ μλ μ¬μ΄νΈμ© βββββββββββββββββββββββββββββββ
# same-originμ λμΌ μ¬μ΄νΈ λ΄μμλ§ μ 체 referrer μ μ‘
referrer_sensitive = "same-origin"
# ββ μμλ³ μ¬μ μ ββββββββββββββββββββββββββββββββββββββββ
# κ°λ³ μμμλ referrer μ μ±
μ μ€μ ν μ μμ΅λλ€:
# <a href="..." referrerpolicy="no-referrer">μΈλΆ λ§ν¬</a>
# <img src="..." referrerpolicy="no-referrer">
# <script src="..." referrerpolicy="no-referrer">
7. Permissions-Policy¶
7.1 λΈλΌμ°μ κΈ°λ₯ μ ν¶
Permissions-Policy (μ΄μ Feature-Policy)λ νμ΄μ§μ ν¬ν¨λ μ½ν μΈ κ° μ¬μ©ν μ μλ λΈλΌμ°μ κΈ°λ₯ λ° APIλ₯Ό μ μ΄ν©λλ€.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Permissions-Policy β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β λΈλΌμ°μ API μ κ·Ό μ μ΄: β
β β
β ββββββββββββββ ββββββββββββββ ββββββββββββββ ββββββββββββββ β
β β Camera β β Microphone β β Geolocationβ β Payment β β
β β camera β β microphone β β geolocationβ β payment β β
β ββββββββββββββ ββββββββββββββ ββββββββββββββ ββββββββββββββ β
β β
β ββββββββββββββ ββββββββββββββ ββββββββββββββ ββββββββββββββ β
β β Fullscreen β β Autoplay β β USB β β Bluetooth β β
β β fullscreen β β autoplay β β usb β β bluetooth β β
β ββββββββββββββ ββββββββββββββ ββββββββββββββ ββββββββββββββ β
β β
β λ¬Έλ²: β
β Permissions-Policy: feature=(allowlist) β
β β
β νμ© λͺ©λ‘ κ°: β
β * = λͺ¨λ μΆμ² νμ© β
β self = λμΌ μΆμ²λ§ νμ© β
β (λΉμ΄ μμ) = ()λ μμ ν μ°¨λ¨μ μλ―Έ β
β "origin" = νΉμ μΆμ² νμ© β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
7.2 κ΅¬μ± μμ ¶
"""
Permissions-Policy ν€λ ꡬμ±.
"""
# ββ μ νμ μ μ±
(λλΆλΆμ μ¬μ΄νΈμ κΆμ₯) βββββββββββββ
permissions_policy = (
"camera=(), " # μΉ΄λ©λΌ μ°¨λ¨
"microphone=(), " # λ§μ΄ν¬ μ°¨λ¨
"geolocation=(), " # μμΉμ 보 μ°¨λ¨
"payment=(), " # Payment API μ°¨λ¨
"usb=(), " # WebUSB μ°¨λ¨
"bluetooth=(), " # Web Bluetooth μ°¨λ¨
"magnetometer=(), " # μλ ₯κ³ μ°¨λ¨
"gyroscope=(), " # μμ΄λ‘μ€μ½ν μ°¨λ¨
"accelerometer=(), " # κ°μλκ³ μ°¨λ¨
'autoplay=(self), ' # λμΌ μΆμ²μμλ§ μλμ¬μ νμ©
'fullscreen=(self), ' # λμΌ μΆμ²μμλ§ μ 체νλ©΄ νμ©
'picture-in-picture=(self)' # λμΌ μΆμ²μμλ§ PiP νμ©
)
# ββ νμ νμ μ±μ© μ μ±
βββββββββββββββββββββββββββββ
permissions_video_app = (
'camera=(self "https://meet.example.com"), '
'microphone=(self "https://meet.example.com"), '
"geolocation=(), "
'fullscreen=(self), '
'display-capture=(self)'
)
# ββ μ μμκ±°λ μ¬μ΄νΈμ© μ μ±
βββββββββββββββββββββββββββββββ
permissions_ecommerce = (
"camera=(), "
"microphone=(), "
'geolocation=(self), ' # λ§€μ₯ μμΉ μ°ΎκΈ°μ©
'payment=(self), ' # Payment Request APIμ©
"usb=(), "
"bluetooth=()"
)
8. Cross-Origin μ μ± (CORP, COEP, COOP)¶
8.1 κ°μ¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Cross-Origin 격리 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β μΈ κ°μ ν€λκ° ν¨κ» cross-origin 격리λ₯Ό μν΄ μλ: β
β β
β CORP (Cross-Origin-Resource-Policy) β
β βββ 리μμ€(μ΄λ―Έμ§, μ€ν¬λ¦½νΈ λ±)μ μ€μ β
β βββ μ΄ λ¦¬μμ€λ₯Ό λ‘λν μ μλ λμ μ μ΄ β
β βββ κ°: same-site, same-origin, cross-origin β
β β
β COEP (Cross-Origin-Embedder-Policy) β
β βββ 리μμ€λ₯Ό ν¬ν¨νλ λ¬Έμμ μ€μ β
β βββ λͺ¨λ 리μμ€κ° μ΅νΈμΈ(CORP λλ CORSλ₯Ό ν΅ν΄)ν΄μΌ ν¨ β
β βββ κ°: unsafe-none, require-corp, credentialless β
β β
β COOP (Cross-Origin-Opener-Policy) β
β βββ λ¬Έμμ μ€μ β
β βββ window.opener κ΄κ³ μ μ΄ β
β βββ κ°: unsafe-none, same-origin, same-origin-allow-popups β
β β
β COEP: require-corp + COOP: same-originμ΄ λͺ¨λ μ€μ λλ©΄: β
β βββΆ νμ΄μ§κ° "cross-origin isolated" β
β βββΆ SharedArrayBuffer, κ³ ν΄μλ νμ΄λ¨Έ νμ±ν β
β βββΆ Spectre μ€νμΌ μ¬μ΄λ μ±λ 곡격μΌλ‘λΆν° λ³΄νΈ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
8.2 Cross-Origin-Resource-Policy (CORP)¶
"""
CORPλ 리μμ€λ₯Ό λ‘λν μ μλ μΆμ²λ₯Ό μ μ΄ν©λλ€.
API μλ΅, μ΄λ―Έμ§, μ€ν¬λ¦½νΈ λ±μ μ€μ ν©λλ€.
"""
# ββ κ° βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# same-origin: λμΌ μΆμ²μ μμ²λ§
# same-site: λμΌ μ¬μ΄νΈμ μμ² (μλΈλλ©μΈ ν¬ν¨)
# cross-origin: λͺ¨λ μΆμ²κ° μ΄ λ¦¬μμ€λ₯Ό λ‘λ κ°λ₯
# λΉκ³΅κ° API μλν¬μΈνΈμ©
corp_api = "same-origin"
# κ³΅κ° CDN 리μμ€μ©
corp_cdn = "cross-origin"
# μλΈλλ©μΈ κ° κ³΅μ λλ 리μμ€μ©
corp_shared = "same-site"
8.3 Cross-Origin-Embedder-Policy (COEP)¶
"""
COEPλ νμ΄μ§μ λ‘λλ λͺ¨λ 리μμ€κ°
cross-originμΌλ‘ λ‘λλλ κ²μ λͺ
μμ μΌλ‘ μ΅νΈμΈνλμ§ νμΈν©λλ€.
"""
# ββ require-corp: κ°μ₯ μ격ν λͺ¨λ ββββββββββββββββββββββββββββββββ
# λͺ¨λ cross-origin 리μμ€λ λ€μ μ€ νλλ₯Ό μνν΄μΌ ν¨:
# 1. CORP: cross-origin ν€λμ ν¨κ» μ 곡
# 2. crossorigin μμ±μΌλ‘ λ‘λ (CORS)
coep_strict = "require-corp"
# HTMLμμ 리μμ€μ crossorigin μμ± νμ:
# <img src="https://cdn.example.com/image.jpg" crossorigin>
# <script src="https://cdn.example.com/lib.js" crossorigin>
# ββ credentialless: λ μ€μ©μ ββββββββββββββββββββββββββββββ
# Cross-origin μμ²μ΄ μ격 μ¦λͺ
(μΏ ν€) μμ΄ μ΄λ£¨μ΄μ§
# 리μμ€μ CORP ν€λ λΆνμ
coep_credentialless = "credentialless"
# ββ unsafe-none: μ ν μμ (κΈ°λ³Έκ°) βββββββββββββββββββββββ
coep_none = "unsafe-none"
8.4 Cross-Origin-Opener-Policy (COOP)¶
"""
COOPλ νμ΄μ§μ opener κ°μ κ΄κ³λ₯Ό μ μ΄ν©λλ€
(window.open λλ λ§ν¬λ₯Ό ν΅ν΄ μ° νμ΄μ§).
"""
# ββ same-origin: μμ 격리 βββββββββββββββββββββββββββββββββ
# cross-origin νμ΄μ§μ λν window.opener μ°Έμ‘° λμ
# cross-origin νμ΄μ§κ° μ΄ μ°½μ μ κ·Όνλ κ²μ λ°©μ§
coop_strict = "same-origin"
# ββ same-origin-allow-popups ββββββββββββββββββββββββββββββββ
# same-originκ³Ό λμΌνμ§λ§, μ΄ νμ΄μ§μμ μ° νμ
μ
# opener μ°Έμ‘°λ₯Ό μ μ§ν μ μμ
coop_popups = "same-origin-allow-popups"
# ββ unsafe-none: μ ν μμ (κΈ°λ³Έκ°) βββββββββββββββββββββββ
coop_none = "unsafe-none"
# ββ cross-origin 격리 λ¬μ± ββββββββββββββββββββββββββββββββ
# λ ν€λ λͺ¨λ νμ:
# Cross-Origin-Embedder-Policy: require-corp
# Cross-Origin-Opener-Policy: same-origin
#
# JavaScriptμμ νμΈ:
# if (crossOriginIsolated) {
# // SharedArrayBuffer μ¬μ© κ°λ₯
# // Performance.now()κ° μμ ν μ λ°λ μ 곡
# }
9. Subresource Integrity (SRI)¶
9.1 SRI μλ λ°©μ¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Subresource Integrity (SRI) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β λ¬Έμ : CDN μΉ¨ν΄ β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β Your App βββββΆβ CDN βββββΆβ Browser β β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β β
β 곡격μκ° CDNμ β
β jquery.min.jsλ₯Ό β
β μμ β
β β
β ν΄κ²°μ±
: SRI ν΄μ νμΈ β
β <script src="https://cdn/jquery.js" β
β integrity="sha384-abc123..." β
β crossorigin="anonymous"> β
β </script> β
β β
β λΈλΌμ°μ κ° νμΌ λ€μ΄λ‘λ βββΆ ν΄μ κ³μ° βββΆ integrityμ λΉκ΅ β
β μΌμΉ? β μ€ν β μ°¨λ¨ λ° μ€λ₯ λ³΄κ³ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
9.2 SRI ν΄μ μμ±¶
# λ‘컬 νμΌμμ SRI ν΄μ μμ±
cat jquery-3.7.1.min.js | openssl dgst -sha384 -binary | openssl base64 -A
# μΆλ ₯: oQVuAfEn...
# shasumμ μ¬μ©νμ¬ SRI ν΄μ μμ±
shasum -b -a 384 jquery-3.7.1.min.js | awk '{ print $1 }' | xxd -r -p | base64
# curlμ μ¬μ©νμ¬ μ격 νμΌμμ ν΄μ μμ±
curl -s https://code.jquery.com/jquery-3.7.1.min.js | \
openssl dgst -sha384 -binary | openssl base64 -A
"""
Pythonμμ SRI ν΄μ μμ± λ° νμΈ.
"""
import hashlib
import base64
import requests
def generate_sri_hash(content: bytes, algorithm: str = 'sha384') -> str:
"""μ£Όμ΄μ§ μ½ν
μΈ μ λν SRI ν΄μ μμ±."""
hash_func = getattr(hashlib, algorithm)
digest = hash_func(content).digest()
b64 = base64.b64encode(digest).decode('utf-8')
return f"{algorithm}-{b64}"
def generate_sri_from_url(url: str) -> str:
"""리μμ€λ₯Ό λ€μ΄λ‘λνκ³ SRI ν΄μ μμ±."""
response = requests.get(url)
response.raise_for_status()
return generate_sri_hash(response.content)
# μ¬μ© μμ
url = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
sri_hash = generate_sri_from_url(url)
print(f'<link rel="stylesheet" href="{url}" '
f'integrity="{sri_hash}" crossorigin="anonymous">')
# ββ HTMLμμ SRI ββββββββββββββββββββββββββββββββββββββββββββββ
# μ€ν¬λ¦½νΈμ©:
# <script src="https://cdn.example.com/lib.js"
# integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
# crossorigin="anonymous"></script>
#
# μ€νμΌμνΈμ©:
# <link rel="stylesheet"
# href="https://cdn.example.com/style.css"
# integrity="sha384-abc123..."
# crossorigin="anonymous">
#
# λ€μ€ ν΄μ (λ§μ΄κ·Έλ μ΄μ
μ©):
# <script src="https://cdn.example.com/lib.js"
# integrity="sha384-oldHash... sha384-newHash..."
# crossorigin="anonymous"></script>
# λΈλΌμ°μ λ μ΄λ€ ν΄μλ μΌμΉνλ©΄ νμ©
# ββ μ€μ μ°Έκ³ μ¬ν ββββββββββββββββββββββββββββββββββββββββββββββ
# 1. cross-origin SRIμλ crossorigin="anonymous" νμ
# 2. SRIλ <script>μ <link rel="stylesheet">μλ§ μλ
# 3. ν΄μλ λ°μ΄νΈ λ¨μλ‘ μΌμΉν΄μΌ ν¨ (곡백λ μ€μ)
# 4. CDNμ΄ νμΌμ μ
λ°μ΄νΈνλ©΄ ν΄μκ° κΉ¨μ§ (μλλ λμ)
10. Flask 보μ ν€λ ꡬ챶
10.1 μλ ν€λ μ€μ ¶
"""
Flask μ ν리μΌμ΄μ
μμ 보μ ν€λ μ€μ .
"""
from flask import Flask, request, make_response, g
import secrets
app = Flask(__name__)
# ββ λ°©λ² 1: after_request λ°μ½λ μ΄ν° ββββββββββββββββββββββββββββ
@app.after_request
def set_security_headers(response):
"""λͺ¨λ μλ΅μ 보μ ν€λ μΆκ°."""
# CSPμ© nonce μμ±
nonce = getattr(g, 'csp_nonce', secrets.token_urlsafe(32))
# Content-Security-Policy
response.headers['Content-Security-Policy'] = (
f"default-src 'self'; "
f"script-src 'self' 'nonce-{nonce}'; "
f"style-src 'self' 'nonce-{nonce}' https://fonts.googleapis.com; "
f"font-src 'self' https://fonts.gstatic.com; "
f"img-src 'self' data:; "
f"connect-src 'self'; "
f"object-src 'none'; "
f"base-uri 'self'; "
f"form-action 'self'; "
f"frame-ancestors 'none'"
)
# HSTS (νλ‘λμ
μμ HTTPSλ₯Ό ν΅ν΄μλ§ μ€μ )
response.headers['Strict-Transport-Security'] = (
'max-age=31536000; includeSubDomains; preload'
)
# MIME μ€λν λ°©μ§
response.headers['X-Content-Type-Options'] = 'nosniff'
# ν΄λ¦μ¬νΉ 보νΈ
response.headers['X-Frame-Options'] = 'DENY'
# Referrer μ μ΄
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
# Permissions μ μ±
response.headers['Permissions-Policy'] = (
'camera=(), microphone=(), geolocation=(), payment=()'
)
# Cross-origin μ μ±
response.headers['Cross-Origin-Opener-Policy'] = 'same-origin'
response.headers['Cross-Origin-Resource-Policy'] = 'same-origin'
# μλ² μλ³ μ κ±°
response.headers.pop('Server', None)
return response
@app.before_request
def generate_nonce():
"""κ° μμ²μ λν CSP nonce μμ±."""
g.csp_nonce = secrets.token_urlsafe(32)
# ββ λ°©λ² 2: Flask-Talisman μ¬μ© ββββββββββββββββββββββββββββββ
# pip install flask-talisman
from flask_talisman import Talisman
app2 = Flask(__name__)
csp = {
'default-src': "'self'",
'script-src': "'self'",
'style-src': "'self' https://fonts.googleapis.com",
'font-src': "'self' https://fonts.gstatic.com",
'img-src': "'self' data:",
'object-src': "'none'",
}
talisman = Talisman(
app2,
content_security_policy=csp,
content_security_policy_nonce_in=['script-src', 'style-src'],
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000,
strict_transport_security_include_subdomains=True,
strict_transport_security_preload=True,
frame_options='DENY',
referrer_policy='strict-origin-when-cross-origin',
permissions_policy={
'camera': '()',
'microphone': '()',
'geolocation': '()',
},
session_cookie_secure=True,
session_cookie_http_only=True,
)
# Jinja2 ν
νλ¦Ώμμ nonce μ¬μ©:
# <script nonce="{{ csp_nonce() }}">
# // μ¬κΈ°μ μΈλΌμΈ JavaScript
# </script>
10.2 λΌμ°νΈλ³ ν€λ μ¬μ μ¶
"""
μλ‘ λ€λ₯Έ λΌμ°νΈμ λν΄ μλ‘ λ€λ₯Έ 보μ ν€λ.
"""
from flask import Flask, make_response
from functools import wraps
app = Flask(__name__)
def custom_csp(csp_string):
"""νΉμ λΌμ°νΈμ λν CSPλ₯Ό μ¬μ μνλ λ°μ½λ μ΄ν°."""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
response = make_response(f(*args, **kwargs))
response.headers['Content-Security-Policy'] = csp_string
return response
return decorated_function
return decorator
@app.route('/admin')
@custom_csp("default-src 'self'; script-src 'self'; frame-ancestors 'none'")
def admin_panel():
"""μ격ν CSPκ° μλ κ΄λ¦¬μ ν¨λ."""
return "Admin Panel"
@app.route('/embed-widget')
@custom_csp(
"default-src 'self'; "
"frame-ancestors 'self' https://partner.example.com"
)
def embeddable_widget():
"""νΉμ ννΈλκ° μλ² λν μ μλ μμ ―."""
return "Widget"
@app.route('/public-api')
def public_api():
"""cross-origin μ κ·Όμ μν΄ μνλ CORPκ° μλ API μλν¬μΈνΈ."""
response = make_response({"data": "value"})
response.headers['Cross-Origin-Resource-Policy'] = 'cross-origin'
response.headers['Access-Control-Allow-Origin'] = '*'
return response
11. Django 보μ ν€λ ꡬ챶
11.1 Django μ€μ ¶
"""
settings.pyμμ Django 보μ ν€λ ꡬμ±.
"""
# ββ λ΄μ₯ Django 보μ μ€μ ββββββββββββββββββββββββββββ
# HSTS
SECURE_HSTS_SECONDS = 31536000 # 1λ
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# HTTPS 리λλ νΈ
SECURE_SSL_REDIRECT = True # HTTPλ₯Ό HTTPSλ‘ λ¦¬λλ νΈ
SECURE_REDIRECT_EXEMPT = [] # 리λλ νΈμμ μ μΈν κ²½λ‘
# Content-Type μ€λν
SECURE_CONTENT_TYPE_NOSNIFF = True # X-Content-Type-Options: nosniff
# ν΄λ¦μ¬νΉ 보νΈ
X_FRAME_OPTIONS = 'DENY' # λ΄μ₯ λ―Έλ€μ¨μ΄
# Referrer μ μ±
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
# Cross-origin opener μ μ±
SECURE_CROSS_ORIGIN_OPENER_POLICY = 'same-origin'
# μΏ ν€ λ³΄μ
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
# ββ νμν λ―Έλ€μ¨μ΄ ββββββββββββββββββββββββββββββββββββββββββ
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # λ°λμ 첫 λ²μ§Έ
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# ... λ€λ₯Έ λ―Έλ€μ¨μ΄
]
11.2 django-cspλ₯Ό μ¬μ©ν Django CSP¶
"""
django-csp ν¨ν€μ§λ₯Ό μ¬μ©ν CSP ꡬμ±.
pip install django-csp
"""
# settings.py
MIDDLEWARE = [
'csp.middleware.CSPMiddleware',
# ... λ€λ₯Έ λ―Έλ€μ¨μ΄
]
# CSP μ§μλ¬Έ
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "https://fonts.googleapis.com")
CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com")
CSP_IMG_SRC = ("'self'", "data:")
CSP_CONNECT_SRC = ("'self'",)
CSP_OBJECT_SRC = ("'none'",)
CSP_BASE_URI = ("'self'",)
CSP_FORM_ACTION = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",)
# script-srcμ style-srcμ nonce νμ±ν
CSP_INCLUDE_NONCE_IN = ['script-src', 'style-src']
# μλ° λ³΄κ³
CSP_REPORT_URI = '/csp-report/'
# Report-only λͺ¨λ (ν
μ€νΈμ©)
# CSP_REPORT_ONLY = True
# ββ Django ν
νλ¦Ώμμ ββββββββββββββββββββββββββββββββββββββββββ
# {% load csp %}
#
# <script nonce="{% csp_nonce %}">
# // μ΄ μΈλΌμΈ μ€ν¬λ¦½νΈλ nonceμ μν΄ νμ©λ¨
# console.log('Hello from a nonced script');
# </script>
#
# ββ λ·°λ³ CSP μ¬μ μ ββββββββββββββββββββββββββββββββββββββββββββ
# from csp.decorators import csp_update, csp_replace, csp_exempt
#
# @csp_update(SCRIPT_SRC=("'self'", "https://cdn.example.com"))
# def my_view(request):
# ...
#
# @csp_exempt # μ΄ λ·°μ λν΄ CSP μμ ν λΉνμ±ν
# def legacy_view(request):
# ...
12. 보μ ν€λ ν μ€νΈ¶
12.1 curlλ‘ ν μ€νΈ¶
# ββ λͺ¨λ μλ΅ ν€λ 보기 ββββββββββββββββββββββββββββββββββββ
curl -I https://example.com
# ββ νΉμ 보μ ν€λ νμΈ βββββββββββββββββββββββββββββββββ
curl -sI https://example.com | grep -iE \
'(content-security|strict-transport|x-content-type|x-frame|referrer-policy|permissions-policy|cross-origin)'
# ββ TLS μΈλΆμ 보λ 보기 μν μμΈ μΆλ ₯ βββββββββββββββββββββββ
curl -vI https://example.com 2>&1 | head -40
# ββ HSTS ν
μ€νΈ βββββββββββββββββββββββββββββββββββββββββββββββ
curl -sI https://example.com | grep -i strict-transport
# ββ report-only λͺ¨λμμ CSP ν
μ€νΈ ββββββββββββββββββββββββββββ
curl -sI https://example.com | grep -i content-security-policy
# ββ λλ½λ ν€λ νμΈ βββββββββββββββββββββββββββββββββββββββββββ
HEADERS_TO_CHECK=(
"Content-Security-Policy"
"Strict-Transport-Security"
"X-Content-Type-Options"
"X-Frame-Options"
"Referrer-Policy"
"Permissions-Policy"
)
URL="https://example.com"
echo "Checking security headers for $URL"
echo "=================================="
for header in "${HEADERS_TO_CHECK[@]}"; do
result=$(curl -sI "$URL" | grep -i "^$header:")
if [ -n "$result" ]; then
echo "[PASS] $result"
else
echo "[FAIL] Missing: $header"
fi
done
12.2 Python 보μ ν€λ μ€μΊλ¶
"""
κ°λ¨ν 보μ ν€λ μ€μΊλ.
"""
import requests
from dataclasses import dataclass
from typing import Optional
@dataclass
class HeaderCheck:
name: str
present: bool
value: Optional[str]
severity: str # 'critical', 'high', 'medium', 'low'
recommendation: str
def scan_security_headers(url: str) -> list[HeaderCheck]:
"""URLμ 보μ ν€λλ₯Ό μ€μΊνκ³ κ²°κ³Ό λ°ν."""
response = requests.get(url, allow_redirects=True, timeout=10)
headers = response.headers
results = []
# ββ Content-Security-Policy ββββββββββββββββββββββββββββββββββ
csp = headers.get('Content-Security-Policy')
results.append(HeaderCheck(
name='Content-Security-Policy',
present=csp is not None,
value=csp,
severity='critical',
recommendation=(
"CSP ν€λλ₯Ό μΆκ°νμΈμ. λ€μμΌλ‘ μμ: "
"Content-Security-Policy: default-src 'self'; "
"object-src 'none'; base-uri 'self'"
) if not csp else _analyze_csp(csp)
))
# ββ Strict-Transport-Security ββββββββββββββββββββββββββββββββ
hsts = headers.get('Strict-Transport-Security')
results.append(HeaderCheck(
name='Strict-Transport-Security',
present=hsts is not None,
value=hsts,
severity='critical',
recommendation=(
"HSTS ν€λλ₯Ό μΆκ°νμΈμ: "
"Strict-Transport-Security: max-age=31536000; "
"includeSubDomains; preload"
) if not hsts else _analyze_hsts(hsts)
))
# ββ X-Content-Type-Options βββββββββββββββββββββββββββββββββββ
xcto = headers.get('X-Content-Type-Options')
results.append(HeaderCheck(
name='X-Content-Type-Options',
present=xcto is not None,
value=xcto,
severity='high',
recommendation=(
"ν€λλ₯Ό μΆκ°νμΈμ: X-Content-Type-Options: nosniff"
) if not xcto else "OK"
))
# ββ X-Frame-Options βββββββββββββββββββββββββββββββββββββββββ
xfo = headers.get('X-Frame-Options')
results.append(HeaderCheck(
name='X-Frame-Options',
present=xfo is not None,
value=xfo,
severity='high',
recommendation=(
"ν€λλ₯Ό μΆκ°νμΈμ: X-Frame-Options: DENY "
"(λλ νλ μ΄λ°μ΄ νμν κ²½μ° SAMEORIGIN)"
) if not xfo else "OK"
))
# ββ Referrer-Policy ββββββββββββββββββββββββββββββββββββββββββ
rp = headers.get('Referrer-Policy')
results.append(HeaderCheck(
name='Referrer-Policy',
present=rp is not None,
value=rp,
severity='medium',
recommendation=(
"ν€λλ₯Ό μΆκ°νμΈμ: Referrer-Policy: "
"strict-origin-when-cross-origin"
) if not rp else "OK"
))
# ββ Permissions-Policy βββββββββββββββββββββββββββββββββββββββ
pp = headers.get('Permissions-Policy')
results.append(HeaderCheck(
name='Permissions-Policy',
present=pp is not None,
value=pp,
severity='medium',
recommendation=(
"ν€λλ₯Ό μΆκ°νμΈμ: Permissions-Policy: "
"camera=(), microphone=(), geolocation=()"
) if not pp else "OK"
))
# ββ μνν ν€λ νμΈ βββββββββββββββββββββββββββββββββββββ
server = headers.get('Server')
if server:
results.append(HeaderCheck(
name='Server',
present=True,
value=server,
severity='low',
recommendation=(
f"Server ν€λκ° λ€μμ λ
ΈμΆ: '{server}'. "
"μ κ±° λλ λλ
νλ₯Ό κ³ λ €νμΈμ."
)
))
x_powered = headers.get('X-Powered-By')
if x_powered:
results.append(HeaderCheck(
name='X-Powered-By',
present=True,
value=x_powered,
severity='medium',
recommendation=(
f"X-Powered-Byκ° λ€μμ λ
ΈμΆ: '{x_powered}'. "
"μ 보 곡κ°λ₯Ό νΌνκΈ° μν΄ μ΄ ν€λλ₯Ό μ κ±°νμΈμ."
)
))
return results
def _analyze_csp(csp: str) -> str:
"""μΌλ°μ μΈ μ½μ μ λν CSP μ μ±
λΆμ."""
issues = []
if "'unsafe-inline'" in csp and 'script-src' in csp:
issues.append("script-srcκ° 'unsafe-inline' νμ© (XSS λ³΄νΈ μ½ν)")
if "'unsafe-eval'" in csp:
issues.append("μ μ±
μ΄ 'unsafe-eval' νμ© (eval() κΈ°λ° κ³΅κ²© νμ±ν)")
if "default-src" not in csp:
issues.append("default-src λ체 μ§μλ¬Έ λλ½")
if "object-src" not in csp and "default-src 'none'" not in csp:
issues.append("object-src μ§μλ¬Έ λλ½ (Flash/νλ¬κ·ΈμΈ μν)")
if "base-uri" not in csp:
issues.append("base-uri λλ½ (dangling markup injection νμ±ν κ°λ₯)")
return "; ".join(issues) if issues else "OK"
def _analyze_hsts(hsts: str) -> str:
"""μ½μ μ λν HSTS ν€λ λΆμ."""
issues = []
hsts_lower = hsts.lower()
if 'max-age=' in hsts_lower:
# max-age κ° μΆμΆ
import re
match = re.search(r'max-age=(\d+)', hsts_lower)
if match:
max_age = int(match.group(1))
if max_age < 31536000:
issues.append(
f"max-age={max_age}κ° 1λ
(31536000)λ³΄λ€ μμ"
)
if 'includesubdomains' not in hsts_lower:
issues.append("includeSubDomains λλ½")
if 'preload' not in hsts_lower:
issues.append("preload λλ½ (λΈλΌμ°μ preload λͺ©λ‘μ μ ν©νμ§ μμ)")
return "; ".join(issues) if issues else "OK"
# ββ μ¬μ© ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == '__main__':
import sys
url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
print(f"\nμ€μΊ μ€: {url}\n{'=' * 60}")
results = scan_security_headers(url)
for check in results:
status = "PASS" if check.present and check.recommendation == "OK" else "FAIL"
icon = "[+]" if status == "PASS" else "[-]"
print(f"\n{icon} {check.name} [{check.severity.upper()}]")
print(f" κ°: {check.value or '(μ€μ λμ§ μμ)'}")
if check.recommendation != "OK":
print(f" κΆμ₯μ¬ν: {check.recommendation}")
12.3 μ¨λΌμΈ μ€μΊλ¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 보μ ν€λ μ€μΊλ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. SecurityHeaders.com β
β URL: https://securityheaders.com β
β λ±κΈ: A+μμ FκΉμ§ β
β λͺ¨λ μ£Όμ 보μ ν€λ νμΈ β
β β
β 2. Mozilla Observatory β
β URL: https://observatory.mozilla.org β
β CSP λΆμμ ν¬ν¨ν ν¬κ΄μ μΈ μ€μΊ β
β κ°μ μ‘°μΈ μ 곡 β
β β
β 3. CSP Evaluator (Google) β
β URL: https://csp-evaluator.withgoogle.com β
β νΉνλ CSP λΆμ β
β μ°ν λ° μ½μ μλ³ β
β β
β 4. Hardenize β
β URL: https://www.hardenize.com β
β ν€λ + TLS + DNS + μ΄λ©μΌ 보μ ν
μ€νΈ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
13. μμ ν 보μ ν€λ ν νλ¦Ώ¶
13.1 Nginx ꡬ챶
# /etc/nginx/conf.d/security-headers.conf
# μλ² λΈλ‘μ μ΄κ²μ ν¬ν¨
# Content-Security-Policy
add_header Content-Security-Policy
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"
always;
# HSTS (λͺ¨λ κ³³μμ HTTPSκ° μλνλμ§ νμΈν νμλ§ νμ±ν)
add_header Strict-Transport-Security
"max-age=31536000; includeSubDomains; preload"
always;
# MIME μ€λν λ°©μ§
add_header X-Content-Type-Options "nosniff" always;
# ν΄λ¦μ¬νΉ 보νΈ
add_header X-Frame-Options "DENY" always;
# Referrer μ μ±
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permissions μ μ±
add_header Permissions-Policy
"camera=(), microphone=(), geolocation=(), payment=()"
always;
# Cross-origin μ μ±
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
# μλ² λ²μ μ 보 μ κ±°
server_tokens off;
# X-Powered-By μ κ±° (μ
μ€νΈλ¦Ό μ±μμ μ€μ ν κ²½μ°)
proxy_hide_header X-Powered-By;
13.2 Apache ꡬ챶
# .htaccess λλ httpd.conf
# Content-Security-Policy
Header always set Content-Security-Policy "\
default-src 'self'; \
script-src 'self'; \
style-src 'self' 'unsafe-inline'; \
img-src 'self' data:; \
font-src 'self'; \
connect-src 'self'; \
object-src 'none'; \
base-uri 'self'; \
form-action 'self'; \
frame-ancestors 'none'"
# HSTS
Header always set Strict-Transport-Security \
"max-age=31536000; includeSubDomains; preload"
# κΈ°ν 보μ ν€λ
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy \
"camera=(), microphone=(), geolocation=(), payment=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Resource-Policy "same-origin"
# μλ² μ 보 μ κ±°
ServerTokens Prod
Header always unset X-Powered-By
14. μ°μ΅ λ¬Έμ ¶
μ°μ΅ λ¬Έμ 1: CSP μ μ± λΆμ¶
λ€μ CSP μ μ± μ λΆμνκ³ λͺ¨λ 보μ μ½μ μ μλ³νμΈμ:
Content-Security-Policy:
default-src *;
script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
style-src 'self' 'unsafe-inline';
img-src *;
connect-src *;
font-src *
μ§λ¬Έ:
1. λͺ κ°μ λ³κ°μ 보μ λ¬Έμ λ₯Ό μλ³ν μ μμ΅λκΉ?
2. κ° λ¬Έμ μ λν΄ νμ±νλλ 곡격 벑ν°λ₯Ό μ€λͺ
νμΈμ.
3. Google Fontsμ λ¨μΌ CDN (cdn.example.com)μ νμ©νλ©΄μ μμ νλλ‘ μ μ±
μ λ€μ μμ±νμΈμ.
μ°μ΅ λ¬Έμ 2: Flask 보μ ν€λ λ―Έλ€μ¨μ΄¶
λ€μμ μννλ Flask λ―Έλ€μ¨μ΄ ν΄λμ€λ₯Ό ꡬμΆνμΈμ:
- λͺ¨λ κΆμ₯ 보μ ν€λ μ€μ
- μμ²λΉ κ³ μ ν CSP nonce μμ±
- Jinja2 ν νλ¦Ώμμ nonce μ¬μ© κ°λ₯νκ² λ§λ€κΈ°
- λ°μ½λ μ΄ν°λ₯Ό ν΅ν λΌμ°νΈλ³ CSP μ¬μ μ νμ©
- CSP ν μ€νΈλ₯Ό μν "report-only" λͺ¨λ μ§μ
- ν€λ κ΅¬μ± μ€λ₯ λ‘κΉ
μ°μ΅ λ¬Έμ 3: ν€λ μ€μΊλ ν₯μ¶
μΉμ 12.2μ Python 보μ ν€λ μ€μΊλλ₯Ό νμ₯νμ¬ λ€μμ μννμΈμ:
Cross-Origin-Embedder-PolicyμCross-Origin-Opener-PolicyνμΈ- HSTS max-ageκ° μ΅μ 1λ μΈμ§ νμΈ
X-Powered-ByλλServerν€λκ° λ²μ μ 보λ₯Ό λ ΈμΆνλμ§ κ°μ§unsafe-inlineλ°unsafe-evalμ¬μ©μ μν΄ CSP νμ± λ° λΆμ- μ€μΊ κ²°κ³Όλ₯Ό κΈ°λ°μΌλ‘ λ±κΈ μμ± (A+μμ FκΉμ§)
- ν μ€νΈ λ° JSON νμμΌλ‘ κ²°κ³Ό μΆλ ₯
μ°μ΅ λ¬Έμ 4: SRI ν΄μ μμ±κΈ°¶
λ€μμ μννλ Python μ€ν¬λ¦½νΈλ₯Ό μμ±νμΈμ:
- CDN URL λͺ©λ‘μ μ λ ₯μΌλ‘ λ°κΈ°
- κ° λ¦¬μμ€ λ€μ΄λ‘λ
- SHA-384 λ¬΄κ²°μ± ν΄μ κ³μ°
- λ¬΄κ²°μ± μμ±μ΄ μλ μμ ν HTML
<script>λλ<link>νκ·Έ μμ± - μ νμ μΌλ‘ λͺ¨λ νκ·Έκ° μλ HTML νμΌ μμ±
- μ€λ₯λ₯Ό μ°μνκ² μ²λ¦¬ (λ€νΈμν¬ μ€ν¨, 404 λ±)
μ°μ΅ λ¬Έμ 5: HSTS Preload μ€λΉ νμΈ¶
λλ©μΈμ΄ HSTS preload μ μΆ μ€λΉκ° λμλμ§ νμΈνλ λꡬλ₯Ό μμ±νμΈμ:
- λλ©μΈμ μ ν¨ν TLS μΈμ¦μκ° μλμ§ νμΈ
- HTTPκ° HTTPSλ‘ λ¦¬λλ νΈλλμ§ νμΈ
- HSTS ν€λμ
max-age >= 31536000μ΄ ν¬ν¨λμ΄ μλμ§ νμΈ includeSubDomainsλ°preloadμ§μλ¬Έ νμΈ- HTTPS μ§μμ μν΄ μΌλ°μ μΈ μλΈλλ©μΈ(www, mail, api) ν μ€νΈ
- μ€λΉ λ³΄κ³ μ μμ±
μ°μ΅ λ¬Έμ 6: Cross-Origin 격리 κ°μ¬¶
μ£Όμ΄μ§ μΉ μ ν리μΌμ΄μ URLμ λν΄:
Cross-Origin-Embedder-Policyκ° μ€μ λμ΄ μλμ§ νμΈCross-Origin-Opener-Policyκ° μ€μ λμ΄ μλμ§ νμΈ- νμ΄μ§μμ λ‘λλ λͺ¨λ cross-origin 리μμ€ μλ³
- κ° cross-origin 리μμ€μ λν΄
Cross-Origin-Resource-Policyκ° μ€μ λμ΄ μλμ§ νμΈ - COEP: require-corpκ° νμ±νλ κ²½μ° κΉ¨μ§ λ¦¬μμ€ λ³΄κ³
μμ½¶
| ν€λ | λ°©μ§νλ 곡격 | νμ? |
|---|---|---|
| Content-Security-Policy | XSS, μ£Όμ | μ |
| Strict-Transport-Security | νλ‘ν μ½ λ€μ΄κ·Έλ μ΄λ | μ (HTTPS μ¬μ΄νΈ) |
| X-Content-Type-Options | MIME νΌλ | μ |
| X-Frame-Options | ν΄λ¦μ¬νΉ | μ |
| Referrer-Policy | μ 보 μ μΆ | μ |
| Permissions-Policy | κΈ°λ₯ λ¨μ© | κΆμ₯ |
| CORP/COEP/COOP | Spectre, cross-origin | κΆμ₯ |
ν΅μ¬ μμ ¶
- report-only λͺ¨λλ‘ μμ β CSPλ₯Ό λ¨Όμ report-onlyλ‘ λ°°ν¬νκ³ , μλ°μ μμ ν ν κ°μ μ μ©
- unsafe-inlineλ³΄λ€ nonce μ¬μ© β nonceλ μΈλΌμΈ μ€ν¬λ¦½νΈμ λν μμ²λ³ μΈμ¦ μ 곡
- HSTSλ μ μ§μ λ‘€μμ νμ β μ§§μ max-ageλ‘ μμνκ³ λͺ¨λ μλΈλλ©μΈμ΄ HTTPSλ₯Ό μ§μνλμ§ νμΈν ν μ¦κ°
- μ¬μΈ΅ λ°©μ΄ β 보μ ν€λλ μμ ν μ½λ© κ΄νμ 보μνλ©° λ체νμ§ μμ
- μ κΈ°μ μΌλ‘ ν μ€νΈ β CI/CDμμ μλνλ μ€μΊλλ₯Ό μ¬μ©νμ¬ ν€λ νκ· ν¬μ°©
μ΄μ : 08. Injection 곡격과 λ°©μ΄ | λ€μ: 10_API_Security.md