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 미듀웨어 클래슀λ₯Ό κ΅¬μΆ•ν•˜μ„Έμš”:

  1. λͺ¨λ“  ꢌμž₯ λ³΄μ•ˆ 헀더 μ„€μ •
  2. μš”μ²­λ‹Ή κ³ μœ ν•œ CSP nonce 생성
  3. Jinja2 ν…œν”Œλ¦Ώμ—μ„œ nonce μ‚¬μš© κ°€λŠ₯ν•˜κ²Œ λ§Œλ“€κΈ°
  4. λ°μ½”λ ˆμ΄ν„°λ₯Ό ν†΅ν•œ λΌμš°νŠΈλ³„ CSP μž¬μ •μ˜ ν—ˆμš©
  5. CSP ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•œ "report-only" λͺ¨λ“œ 지원
  6. 헀더 ꡬ성 였λ₯˜ λ‘œκΉ…

μ—°μŠ΅ 문제 3: 헀더 μŠ€μΊλ„ˆ ν–₯상

μ„Ήμ…˜ 12.2의 Python λ³΄μ•ˆ 헀더 μŠ€μΊλ„ˆλ₯Ό ν™•μž₯ν•˜μ—¬ λ‹€μŒμ„ μˆ˜ν–‰ν•˜μ„Έμš”:

  1. Cross-Origin-Embedder-Policy와 Cross-Origin-Opener-Policy 확인
  2. HSTS max-ageκ°€ μ΅œμ†Œ 1년인지 확인
  3. X-Powered-By λ˜λŠ” Server 헀더가 버전 정보λ₯Ό λ…ΈμΆœν•˜λŠ”μ§€ 감지
  4. unsafe-inline 및 unsafe-eval μ‚¬μš©μ„ μœ„ν•΄ CSP νŒŒμ‹± 및 뢄석
  5. μŠ€μΊ” κ²°κ³Όλ₯Ό 기반으둜 λ“±κΈ‰ 생성 (A+μ—μ„œ FκΉŒμ§€)
  6. ν…μŠ€νŠΈ 및 JSON ν˜•μ‹μœΌλ‘œ κ²°κ³Ό 좜λ ₯

μ—°μŠ΅ 문제 4: SRI ν•΄μ‹œ 생성기

λ‹€μŒμ„ μˆ˜ν–‰ν•˜λŠ” Python 슀크립트λ₯Ό μž‘μ„±ν•˜μ„Έμš”:

  1. CDN URL λͺ©λ‘μ„ μž…λ ₯으둜 λ°›κΈ°
  2. 각 λ¦¬μ†ŒμŠ€ λ‹€μš΄λ‘œλ“œ
  3. SHA-384 무결성 ν•΄μ‹œ 계산
  4. 무결성 속성이 μžˆλŠ” μ™„μ „ν•œ HTML <script> λ˜λŠ” <link> νƒœκ·Έ 생성
  5. μ„ νƒμ μœΌλ‘œ λͺ¨λ“  νƒœκ·Έκ°€ μžˆλŠ” HTML 파일 μž‘μ„±
  6. 였λ₯˜λ₯Ό μš°μ•„ν•˜κ²Œ 처리 (λ„€νŠΈμ›Œν¬ μ‹€νŒ¨, 404 λ“±)

μ—°μŠ΅ 문제 5: HSTS Preload μ€€λΉ„ 확인

도메인이 HSTS preload 제좜 μ€€λΉ„κ°€ λ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜λŠ” 도ꡬλ₯Ό μž‘μ„±ν•˜μ„Έμš”:

  1. 도메인에 μœ νš¨ν•œ TLS μΈμ¦μ„œκ°€ μžˆλŠ”μ§€ 확인
  2. HTTPκ°€ HTTPS둜 λ¦¬λ””λ ‰νŠΈλ˜λŠ”μ§€ 확인
  3. HSTS 헀더에 max-age >= 31536000이 ν¬ν•¨λ˜μ–΄ μžˆλŠ”μ§€ 확인
  4. includeSubDomains 및 preload μ§€μ‹œλ¬Έ 확인
  5. HTTPS 지원을 μœ„ν•΄ 일반적인 μ„œλΈŒλ„λ©”μΈ(www, mail, api) ν…ŒμŠ€νŠΈ
  6. μ€€λΉ„ λ³΄κ³ μ„œ 생성

μ—°μŠ΅ 문제 6: Cross-Origin 격리 감사

μ£Όμ–΄μ§„ μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜ URL에 λŒ€ν•΄:

  1. Cross-Origin-Embedder-Policyκ°€ μ„€μ •λ˜μ–΄ μžˆλŠ”μ§€ 확인
  2. Cross-Origin-Opener-Policyκ°€ μ„€μ •λ˜μ–΄ μžˆλŠ”μ§€ 확인
  3. νŽ˜μ΄μ§€μ—μ„œ λ‘œλ“œλœ λͺ¨λ“  cross-origin λ¦¬μ†ŒμŠ€ 식별
  4. 각 cross-origin λ¦¬μ†ŒμŠ€μ— λŒ€ν•΄ Cross-Origin-Resource-Policyκ°€ μ„€μ •λ˜μ–΄ μžˆλŠ”μ§€ 확인
  5. 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 ꢌμž₯

핡심 μš”μ 

  1. report-only λͺ¨λ“œλ‘œ μ‹œμž‘ β€” CSPλ₯Ό λ¨Όμ € report-only둜 λ°°ν¬ν•˜κ³ , μœ„λ°˜μ„ μˆ˜μ •ν•œ ν›„ κ°•μ œ 적용
  2. unsafe-inline보닀 nonce μ‚¬μš© β€” nonceλŠ” 인라인 μŠ€ν¬λ¦½νŠΈμ— λŒ€ν•œ μš”μ²­λ³„ 인증 제곡
  3. HSTSλŠ” 점진적 둀아웃 ν•„μš” β€” 짧은 max-age둜 μ‹œμž‘ν•˜κ³  λͺ¨λ“  μ„œλΈŒλ„λ©”μΈμ΄ HTTPSλ₯Ό μ§€μ›ν•˜λŠ”μ§€ ν™•μΈν•œ ν›„ 증가
  4. 심측 λ°©μ–΄ β€” λ³΄μ•ˆ ν—€λ”λŠ” μ•ˆμ „ν•œ μ½”λ”© 관행을 λ³΄μ™„ν•˜λ©° λŒ€μ²΄ν•˜μ§€ μ•ŠμŒ
  5. μ •κΈ°μ μœΌλ‘œ ν…ŒμŠ€νŠΈ β€” CI/CDμ—μ„œ μžλ™ν™”λœ μŠ€μΊλ„ˆλ₯Ό μ‚¬μš©ν•˜μ—¬ 헀더 νšŒκ·€ 포착

이전: 08. Injection 곡격과 λ°©μ–΄ | λ‹€μŒ: 10_API_Security.md

to navigate between lessons