๐Ÿ›ก๏ธ HTTP ๋ณด์•ˆ ํ—ค๋” ์ ๊ฒ€: ๋‚ด ๋ธ”๋กœ๊ทธ ์‘๋‹ต์—๋Š” HSTS ํ•˜๋‚˜๋งŒ ๋ถ™์–ด ์žˆ์—ˆ๋‹ค

@leekh8 ยท October 13, 2026 ยท 13 min read

์›น ๋ณด์•ˆ ๊ธฐ์ดˆ ์‹œ๋ฆฌ์ฆˆ


OWASP Top 10 ๊ธ€์—์„œ "๋ณด์•ˆ ํ—ค๋” ์„ค์ •"์„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ•œ ์ค„๋กœ ์ ์—ˆ๋‹ค. ๊ทธ ํ•œ ์ค„์„ ์ด ๋ธ”๋กœ๊ทธ์— ์ง์ ‘ ๋Œ€ ๋ณด์•˜๋‹ค.

$ curl -sI https://leekh8.github.io/
HTTP/1.1 200 OK
Server: GitHub.com
Content-Type: text/html; charset=utf-8
Access-Control-Allow-Origin: *
Strict-Transport-Security: max-age=31556952
Cache-Control: max-age=600
...

๋ณด์•ˆ ํ—ค๋”๋ผ๊ณ  ๋ถ€๋ฅผ ๋งŒํ•œ ๊ฒƒ์€ Strict-Transport-Security ํ•˜๋‚˜๋‹ค. ๊ฐ™์€ ๋‚  github.com์„ ์ฐ์œผ๋ฉด ์ด๋ ‡๋‹ค.

$ curl -sI https://github.com/
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
X-Frame-Options: deny
X-Content-Type-Options: nosniff
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Content-Security-Policy: default-src 'none'; base-uri 'self'; ...
Set-Cookie: _gh_sess=...; path=/; secure; HttpOnly; SameSite=Lax

๊ฐ™์€ ํšŒ์‚ฌ๊ฐ€ ์šด์˜ํ•˜๋Š” ์„œ๋ฒ„์ธ๋ฐ ์ฐจ์ด๊ฐ€ ํฌ๋‹ค. ์ด์œ ๋Š” ๋’ค์—์„œ ๋‹ค๋ฃฌ๋‹ค. ๊ฒฐ๋ก ๋ถ€ํ„ฐ ์ ์œผ๋ฉด GitHub Pages๋Š” ์‘๋‹ต ํ—ค๋”๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ”๊ฟ€ ์ˆ˜ ์—†๋‹ค. ๊ทธ๋ž˜์„œ ์ด ๊ธ€์€ ํ—ค๋”๋งˆ๋‹ค ๋ฌด์—‡์„ ๋ง‰๋Š”์ง€ ์ •๋ฆฌํ•˜๊ณ , ํ—ค๋”๋ฅผ ๋ชป ๊ฑฐ๋Š” ํ™˜๊ฒฝ์—์„œ ์–ด๋””๊นŒ์ง€ ๋ฉ”์šธ ์ˆ˜ ์žˆ๋Š”์ง€๋ฅผ ๋”ฐ๋กœ ๋‹ค๋ฃฌ๋‹ค.

๋ณด์•ˆ ํ—ค๋”๊ฐ€ ํ•˜๋Š” ์ผ

๋ณด์•ˆ ํ—ค๋”๋Š” ์„œ๋ฒ„๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์—๊ฒŒ "์ด ํŽ˜์ด์ง€๋Š” ์ด๋ ‡๊ฒŒ ๋‹ค๋ค„๋ผ"๋ผ๊ณ  ์ง€์‹œํ•˜๋Š” ๊ฐ’์ด๋‹ค. ์„œ๋ฒ„ ์ฝ”๋“œ์˜ ์ทจ์•ฝ์ ์„ ๊ณ ์น˜์ง€๋Š” ์•Š๋Š”๋‹ค. ์ทจ์•ฝ์ ์ด ์žˆ์–ด๋„ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ณต๊ฒฉ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ฒŒ ๋ง‰๋Š” ๋งˆ์ง€๋ง‰ ์ธต์ด๋‹ค.

HSTS
CSP
X-Frame-Options

frame-ancestors
nosniff
Referrer-Policy
Permissions-Policy
์„œ๋ฒ„ ์‘๋‹ต
์‘๋‹ต ํ—ค๋”
http๋กœ ์ ‘์† ์‹œ๋„ ์ฐจ๋‹จ
ํ—ˆ์šฉ ์•ˆ ํ•œ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ์ฐจ๋‹จ
๋‹ค๋ฅธ ์‚ฌ์ดํŠธ iframe ์‚ฝ์ž… ์ฐจ๋‹จ
MIME ์ถ”์ธก ์‹คํ–‰ ์ฐจ๋‹จ
์ฃผ์†Œ ์œ ์ถœ ๋ฒ”์œ„ ์ œํ•œ
์นด๋ฉ”๋ผ, ์œ„์น˜ ๋“ฑ API ์‚ฌ์šฉ ์ œํ•œ

ํ—ค๋” ๋ง‰๋Š” ๊ณต๊ฒฉ ๊ถŒ์žฅ๊ฐ’ ์ด ๋ธ”๋กœ๊ทธ
Strict-Transport-Security ๋‹ค์šด๊ทธ๋ ˆ์ด๋“œ, ์ฟ ํ‚ค ํƒˆ์ทจ max-age=31536000; includeSubDomains โš ๏ธ max-age๋งŒ
Content-Security-Policy XSS, ๋ฐ์ดํ„ฐ ์œ ์ถœ ์‚ฌ์ดํŠธ๋ณ„ ์„ค๊ณ„ โŒ
X-Frame-Options ํด๋ฆญ์žฌํ‚น DENY โŒ
X-Content-Type-Options MIME ์Šค๋‹ˆํ•‘ nosniff โŒ
Referrer-Policy URL ์† ํ† ํฐ ์œ ์ถœ strict-origin-when-cross-origin โŒ (๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ณธ๊ฐ’)
Permissions-Policy ์‚ฝ์ž…๋œ ์ฝ”๋“œ์˜ ๊ธฐ๊ธฐ API ๋‚จ์šฉ camera=(), microphone=(), geolocation=() โŒ

๊ถŒ์žฅ๊ฐ’์€ OWASP Secure Headers Projectowasp-shp์™€ MDNmdn-headers์„ ๋”ฐ๋ž๋‹ค.

HSTS: ์ฒซ ์ ‘์† ํ•œ ๋ฒˆ์€ ๋ชป ๋ง‰๋Š”๋‹ค

Strict-Transport-Security๋ฅผ ๋ฐ›์€ ๋ธŒ๋ผ์šฐ์ €๋Š” max-age ์ดˆ ๋™์•ˆ ๊ทธ ๋„๋ฉ”์ธ์— http๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด์ง€ ์•Š๊ณ  ๋‚ด๋ถ€์—์„œ https๋กœ ๋ฐ”๊พผ๋‹คrfc6797. ๊ณต์šฉ ์™€์ดํŒŒ์ด์—์„œ http:// ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„ ๊ฐ€์งœ ํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ ์ฃผ๋Š” ๊ณต๊ฒฉ(SSL stripping)์ด ์—ฌ๊ธฐ์„œ ๋ง‰ํžŒ๋‹ค.

์•ฝ์ ์€ ์ฒซ ์ ‘์†์ด๋‹ค. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ด ํ—ค๋”๋ฅผ ํ•œ ๋ฒˆ๋„ ๋ฐ›์€ ์ ์ด ์—†์œผ๋ฉด ์ฒซ ์š”์ฒญ์€ http๋กœ ๋‚˜๊ฐ„๋‹ค.

์„œ๋ฒ„์ค‘๊ฐ„์ž๋ธŒ๋ผ์šฐ์ €์„œ๋ฒ„์ค‘๊ฐ„์ž๋ธŒ๋ผ์šฐ์ €์ฒซ ๋ฐฉ๋ฌธ, HSTS ๊ธฐ๋ก ์—†์Œ๋‘ ๋ฒˆ์งธ ๋ฐฉ๋ฌธ๋ถ€ํ„ฐGET http://example.com๊ฐ€์งœ ํŽ˜์ด์ง€ (https๋กœ ์•ˆ ๋„˜๊น€)http๋ฅผ https๋กœ ๋‚ด๋ถ€ ๋ณ€ํ™˜ (307)GET https://example.com200 + Strict-Transport-Security

์ด ์ฒซ ์ ‘์†์„ ๋ง‰๋Š” ๋ฐฉ๋ฒ•์ด preload ๋ชฉ๋ก์ด๋‹ค. ๋ธŒ๋ผ์šฐ์ € ๋ฐฐํฌํŒ ์•ˆ์— "์ด ๋„๋ฉ”์ธ์€ ์ฒ˜์Œ๋ถ€ํ„ฐ https" ๋ชฉ๋ก์„ ๋„ฃ์–ด ๋‘”๋‹ค. ๋“ฑ๋ก ์กฐ๊ฑด์€ hstspreload.org์— ์ ํ˜€ ์žˆ๋‹คhstspreload.

  • http ์š”์ฒญ์„ ๊ฐ™์€ ํ˜ธ์ŠคํŠธ์˜ https๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
  • ๋ชจ๋“  ํ•˜์œ„ ๋„๋ฉ”์ธ์ด https ์ œ๊ณต
  • ๊ธฐ๋ณธ ๋„๋ฉ”์ธ ์‘๋‹ต์— max-age 31536000 ์ด์ƒ, includeSubDomains, preload ์„ธ ๊ฐ’

โš ๏ธ includeSubDomains๋Š” ํ•˜์œ„ ๋„๋ฉ”์ธ์„ ์ „๋ถ€ ํ™•์ธํ•œ ๋’ค์— ๋ถ™์ธ๋‹ค. ์‚ฌ๋‚ด ์‹œ์Šคํ…œ ํ•˜๋‚˜๊ฐ€ http๋กœ๋งŒ ์—ด๋ ค ์žˆ์œผ๋ฉด ๊ทธ ์‹œ์Šคํ…œ์€ max-age ๋™์•ˆ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์—ด๋ฆฌ์ง€ ์•Š๋Š”๋‹ค. preload ๋ชฉ๋ก์€ ๋นผ๋Š” ๋ฐ๋„ ๋ธŒ๋ผ์šฐ์ € ๋ฐฐํฌ ์ฃผ๊ธฐ๋งŒํผ ๊ฑธ๋ฆฐ๋‹ค. max-age=300์ฒ˜๋Ÿผ ์งง๊ฒŒ ๊ฑธ์–ด์„œ ๋ฌธ์ œ๊ฐ€ ์—†๋Š”์ง€ ๋ณธ ๋’ค ๋Š˜๋ฆฌ๋Š” ์ˆœ์„œ๊ฐ€ ์•ˆ์ „ํ•˜๋‹ค.

์ด ๋ธ”๋กœ๊ทธ์˜ max-age=31556952๋Š” 365.2425์ผ, ๊ทธ๋ ˆ๊ณ ๋ฆฌ๋ ฅ 1๋…„์ด๋‹ค. includeSubDomains์™€ preload๋Š” ์—†๋‹ค. *.github.io ํ•˜์œ„ ๋„๋ฉ”์ธ์€ ์‚ฌ์šฉ์ž๋งˆ๋‹ค ์ฃผ์ธ์ด ๋‹ค๋ฅด๋‹ˆ ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฒฐ์ •ํ•  ๊ฐ’์ด ์•„๋‹ˆ๊ณ , preload ๋“ฑ๋ก๋„ ๋“ฑ๋ก ๊ฐ€๋Šฅํ•œ ๋„๋ฉ”์ธ ๋‹จ์œ„๋ผ leekh8.github.io ํ˜ผ์ž์„œ๋Š” ์‹ ์ฒญํ•  ์ˆ˜ ์—†๋‹ค.

CSP: ๊ฐ€์žฅ ๊ฐ•ํ•˜๊ณ  ๊ฐ€์žฅ ์–ด๋ ต๋‹ค

Content-Security-Policy๋Š” ์Šคํฌ๋ฆฝํŠธ, ์Šคํƒ€์ผ, ์ด๋ฏธ์ง€, ์ ‘์† ๋Œ€์ƒ์„ ์–ด๋””์„œ ๋ถˆ๋Ÿฌ์™€๋„ ๋˜๋Š”์ง€ ๋ชฉ๋ก์œผ๋กœ ์ •ํ•œ๋‹คmdn-csp. XSS๋กœ <script> ํ•˜๋‚˜๊ฐ€ ๋ผ์–ด๋“ค์–ด๋„ ๋ชฉ๋ก์— ์—†์œผ๋ฉด ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค.

Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

๋ฌธ์ œ๋Š” ์ธ๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ๋‹ค. CSP๋ฅผ ๊ฑธ๋ฉด HTML ์•ˆ์— ์ง์ ‘ ์“ด <script>...</script>์™€ onclick="..."์ด ๊ธฐ๋ณธ์œผ๋กœ ๋ง‰ํžŒ๋‹ค. ๊ณต๊ฒฉ์ž๊ฐ€ ๋„ฃ๋Š” ์ฝ”๋“œ๋„ ๋Œ€๋ถ€๋ถ„ ์ธ๋ผ์ธ์ด๋ผ์„œ๋‹ค. ์ด ๋ธ”๋กœ๊ทธ์˜ ์ฒซ ํŽ˜์ด์ง€ HTML์„ ์„ธ์–ด ๋ณด์•˜๋‹ค.

$ curl -s https://leekh8.github.io/ -o index.html
$ grep -aoE '<script[^>]*>' index.html | sort | uniq -c
      2 <script>
      1 <script id="gatsby-script-loader">
      1 <script id="redux-ssr">
      1 <script data-react-helmet="true" type="application/ld+json">
      1 <script async="" src="https://www.googletagmanager.com/gtag/js?id=...">
      1 <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle...">
      4 <script src="/...js" async>   # ๋ฒˆ๋“ค 4๊ฐœ, ์ค„์—ฌ์„œ ํ‘œ์‹œ
$ grep -aoE ' style="' index.html | wc -l
20
ํ•ญ๋ชฉ ๊ฐœ์ˆ˜ CSP์—์„œ ์ƒ๊ธฐ๋Š” ์ผ
์‹คํ–‰๋˜๋Š” ์ธ๋ผ์ธ <script> 4 ํ—ˆ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด ํŽ˜์ด์ง€ ๋™์ž‘์ด ๊นจ์ง
JSON-LD (application/ld+json) 1 ์‹คํ–‰๋˜์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ๋ผ ์˜ํ–ฅ ์—†์Œ
์™ธ๋ถ€ ์ถœ์ฒ˜ ์Šคํฌ๋ฆฝํŠธ 2 ์ถœ์ฒ˜๋ฅผ ๋ชฉ๋ก์— ๋„ฃ์–ด์•ผ ํ•จ. ๊ด‘๊ณ  ์Šคํฌ๋ฆฝํŠธ๋Š” ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์—์„œ ๋˜ ๋ถˆ๋Ÿฌ์˜ด
<style> ๋ธ”๋ก 2 style-src ํ—ˆ์šฉ ํ•„์š” (styled-components๊ฐ€ ๋งŒ๋“ฆ)
style="..." ์†์„ฑ 20 style-src์— 'unsafe-inline' ํ•„์š”

๊ฐ€์žฅ ์‰ฌ์šด ํƒˆ์ถœ๊ตฌ๋Š” script-src์— 'unsafe-inline'์„ ๋„ฃ๋Š” ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ํŽ˜์ด์ง€๋Š” ๋‹ค ๋ˆ๋‹ค. ๋Œ€์‹  ๊ณต๊ฒฉ์ž๊ฐ€ ๋„ฃ์€ ์ธ๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ๋„ ๋ˆ๋‹ค. script-src์— 'unsafe-inline'์ด ์žˆ์œผ๋ฉด CSP์˜ XSS ๋ฐฉ์–ด๋Š” ๊ฑฐ์˜ ๋‚จ์ง€ ์•Š๋Š”๋‹ค.

์ธ๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ด๋ฆฌ๋ฉด์„œ ๋ง‰๋Š” ๋ฐฉ๋ฒ•์€ ๋‘ ๊ฐ€์ง€๋‹คwebdev-strict-csp.

๋ฐฉ์‹ ์›๋ฆฌ ์กฐ๊ฑด
nonce ์‘๋‹ต๋งˆ๋‹ค ์ƒˆ ๋‚œ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ํ—ค๋”์™€ <script nonce="...">์— ๊ฐ™์ด ๋„ฃ๋Š”๋‹ค ์‘๋‹ต์„ ์„œ๋ฒ„๊ฐ€ ๋งค๋ฒˆ ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค. ์ •์  ํŒŒ์ผ์—๋Š” ๋ชป ์“ด๋‹ค
hash ์ธ๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ ๋‚ด์šฉ์˜ SHA-256 ๊ฐ’์„ script-src์— ๋„ฃ๋Š”๋‹ค ๋‚ด์šฉ์ด ํ•œ ๊ธ€์ž๋ผ๋„ ๋ฐ”๋€Œ๋ฉด ๊ฐ’๋„ ๋ฐ”๋€๋‹ค. ๋นŒ๋“œํ•  ๋•Œ๋งˆ๋‹ค ๋‹ค์‹œ ๊ณ„์‚ฐํ•ด์•ผ ํ•œ๋‹ค

๋‘˜ ๋‹ค 'strict-dynamic'๊ณผ ๊ฐ™์ด ์“ฐ๋ฉด "ํ—ˆ์šฉ๋œ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋ถˆ๋Ÿฌ์˜จ ์Šคํฌ๋ฆฝํŠธ"๋„ ํ—ˆ์šฉ๋œ๋‹ค. ๊ด‘๊ณ ๋‚˜ ํƒœ๊ทธ ๊ด€๋ฆฌ์ž์ฒ˜๋Ÿผ ์ถœ์ฒ˜๋ฅผ ๋ฏธ๋ฆฌ ๋‹ค ์•Œ ์ˆ˜ ์—†๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถœ์ฒ˜ ๋ชฉ๋ก ์—†์ด ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋‹ค.

โœ… CSP๋ฅผ ์ฒ˜์Œ ๊ฑฐ๋Š” ์ˆœ์„œ๋Š” ์ด๋ ‡๋‹ค.

  1. Content-Security-Policy-Report-Only๋กœ ๊ฑด๋‹ค. ๋ง‰์ง€ ์•Š๊ณ  ์œ„๋ฐ˜๋งŒ ๋ณด๊ณ ํ•œ๋‹ค
  2. ๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”๊ณผ ๋ณด๊ณ  ์ˆ˜์ง‘ ์ฃผ์†Œ์— ์Œ“์ด๋Š” ์œ„๋ฐ˜์„ ๋ณด๊ณ  ๋ชฉ๋ก์„ ๊ณ ์นœ๋‹ค
  3. ์œ„๋ฐ˜์ด ์ •์ƒ ๋™์ž‘์—์„œ ๋” ์•ˆ ๋‚˜์˜ค๋ฉด Content-Security-Policy๋กœ ๋ฐ”๊พผ๋‹ค
Content-Security-Policy-Report-Only: script-src 'self' 'strict-dynamic' 'sha256-...'; object-src 'none'; base-uri 'self'; report-to csp-endpoint

ํด๋ฆญ์žฌํ‚น: X-Frame-Options์™€ frame-ancestors

๊ณต๊ฒฉ์ž ์‚ฌ์ดํŠธ๊ฐ€ ๋‚ด ์‚ฌ์ดํŠธ๋ฅผ ํˆฌ๋ช…ํ•œ <iframe>์œผ๋กœ ๋ฎ๊ณ  ์—‰๋šฑํ•œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๊ฒŒ ๋งŒ๋“œ๋Š” ๊ณต๊ฒฉ์ด ํด๋ฆญ์žฌํ‚น์ด๋‹ค. ๋ง‰๋Š” ํ—ค๋”๋Š” ๋‘˜์ด๋‹ค.

X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'

frame-ancestors๊ฐ€ ๋‚˜์ค‘์— ๋‚˜์˜จ ํ‘œ์ค€์ด๊ณ , ๋‘˜ ๋‹ค ์žˆ์œผ๋ฉด ๋ธŒ๋ผ์šฐ์ €๋Š” frame-ancestors๋ฅผ ๋”ฐ๋ฅธ๋‹คmdn-xfo. X-Frame-Options์˜ ALLOW-FROM์€ ์ง€์›์ด ๋Š๊ฒผ์œผ๋‹ˆ ํŠน์ • ์‚ฌ์ดํŠธ๋งŒ ํ—ˆ์šฉํ•˜๋ ค๋ฉด frame-ancestors https://partner.example์„ ์“ด๋‹ค. ์˜ค๋ž˜๋œ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์œ„ํ•ด ๋‘˜ ๋‹ค ๊ฑฐ๋Š” ํŽธ์ด ๋ฌด๋‚œํ•˜๋‹ค.

nosniff, Referrer-Policy, Permissions-Policy

์„ค์ •์ด ํ•œ ์ค„์ด๊ณ  ๋ถ€์ž‘์šฉ์ด ๊ฑฐ์˜ ์—†๋Š” ์„ธ ๊ฐ€์ง€๋‹ค.

X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
  • nosniff: ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Content-Type์„ ๋ฌด์‹œํ•˜๊ณ  ๋‚ด์šฉ์„ ๋ณด๊ณ  ์ข…๋ฅ˜๋ฅผ ์ถ”์ธกํ•˜๋Š” ๋™์ž‘์„ ๋ˆ๋‹ค. ์—…๋กœ๋“œํ•œ .txt ํŒŒ์ผ ์•ˆ์˜ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์Šคํฌ๋ฆฝํŠธ๋กœ ์‹คํ–‰๋˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋ง‰๋Š”๋‹ค
  • Referrer-Policy: ๋‹ค๋ฅธ ์‚ฌ์ดํŠธ๋กœ ์ด๋™ํ•  ๋•Œ ๋ณด๋‚ด๋Š” Referer์— ์ „์ฒด ์ฃผ์†Œ ๋Œ€์‹  ๋„๋ฉ”์ธ๋งŒ ๋‹ด๋Š”๋‹ค. ์ฃผ์†Œ์— ?token=... ๊ฐ™์€ ๊ฐ’์ด ๋“ค์–ด๊ฐ€๋Š” ์‚ฌ์ดํŠธ์—์„œ ์˜๋ฏธ๊ฐ€ ์žˆ๋‹ค. strict-origin-when-cross-origin์€ Chrome 85, Firefox 87๋ถ€ํ„ฐ ๊ธฐ๋ณธ๊ฐ’์ด๋ผ ํ—ค๋”๊ฐ€ ์—†์–ด๋„ ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €๋Š” ์ด๋ ‡๊ฒŒ ๋™์ž‘ํ•œ๋‹คmdn-referrer. ๊ทธ๋ž˜๋„ ๋ช…์‹œํ•ด ๋‘๋ฉด ๋ธŒ๋ผ์šฐ์ €๋งˆ๋‹ค ๋‹ค๋ฅด๊ฒŒ ๋™์ž‘ํ•  ์—ฌ์ง€๊ฐ€ ์—†๋‹ค
  • Permissions-Policy: ํŽ˜์ด์ง€์™€ ๊ทธ ์•ˆ์˜ iframe์ด ์“ธ ์ˆ˜ ์žˆ๋Š” ๊ธฐ๊ธฐ ๊ธฐ๋Šฅ์„ ์ •ํ•œ๋‹ค. ()๋Š” ์•„๋ฌด์—๊ฒŒ๋„ ํ—ˆ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๋œป์ด๋‹ค. ๊ด‘๊ณ ๋‚˜ ์œ„์ ฏ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์œ„์น˜ ์ •๋ณด๋ฅผ ์š”์ฒญํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋ง‰๋Š”๋‹ค

์ธก์ •ํ•ด ๋ณด๋ฉด github.com๋„ Permissions-Policy๋Š” ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค. ๊ถŒ์žฅ ๋ชฉ๋ก์— ์žˆ๋‹ค๊ณ  ๋‹ค ๋ถ™์–ด ์žˆ์ง€๋Š” ์•Š๋‹ค.

์ฟ ํ‚ค ์†์„ฑ๋„ ํ—ค๋”๋‹ค

Set-Cookie๋„ ์‘๋‹ต ํ—ค๋”๋‹ค. ์„ธ์…˜ ์ฟ ํ‚ค์—๋Š” ์„ธ ์†์„ฑ์ด ๊ธฐ๋ณธ์ด๋‹ค. ๊ฐ ์†์„ฑ์ด ๋ง‰๋Š” ๊ณต๊ฒฉ์€ JWT, OAuth, ์„ธ์…˜ ๋ณด์•ˆ ๊ธ€์—์„œ ๋‹ค๋ค˜๋‹ค.

Set-Cookie: __Host-session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax
์†์„ฑ ํšจ๊ณผ
Secure https ์š”์ฒญ์—๋งŒ ์ฟ ํ‚ค๋ฅผ ๋ณด๋ƒ„
HttpOnly document.cookie๋กœ ๋ชป ์ฝ์Œ. XSS๋กœ ์„ธ์…˜์„ ํ›”์น˜๊ธฐ ์–ด๋ ค์›Œ์ง
SameSite=Lax ๋‹ค๋ฅธ ์‚ฌ์ดํŠธ์—์„œ ์‹œ์ž‘ํ•œ POST ์š”์ฒญ์— ์ฟ ํ‚ค๋ฅผ ์•ˆ ๋ณด๋ƒ„. CSRF ๋ฐฉ์–ด
__Host- ์ ‘๋‘์‚ฌ Secure, Path=/, Domain ์—†์Œ์„ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ฐ•์ œ. ํ•˜์œ„ ๋„๋ฉ”์ธ์ด ์ฟ ํ‚ค๋ฅผ ๋ฎ์–ด์“ฐ์ง€ ๋ชปํ•จmdn-cookie

github.com์˜ _octo ์ฟ ํ‚ค์—๋Š” HttpOnly๊ฐ€ ์—†๋‹ค. ํŽ˜์ด์ง€ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ฝ์–ด์•ผ ํ•˜๋Š” ๋ถ„์„์šฉ ์ฟ ํ‚ค๋ผ์„œ๋‹ค. ์„ธ์…˜์ด๋‚˜ ์ธ์ฆ์— ์“ฐ๋Š” ์ฟ ํ‚ค๋งŒ HttpOnly๊ฐ€ ํ•„์ˆ˜๋‹ค. ์ ๊ฒ€ ๋„๊ตฌ๊ฐ€ ๊ฒฝ๊ณ ๋ฅผ ๋„์›Œ๋„ ์ฟ ํ‚ค ์šฉ๋„๋ฅผ ๋ณด๊ณ  ํŒ๋‹จํ•œ๋‹ค.

์ด์ œ ์“ฐ์ง€ ์•Š๋Š” ํ—ค๋”

์ธํ„ฐ๋„ท์˜ ์˜ค๋ž˜๋œ ์„ค์ • ์˜ˆ์ œ๋ฅผ ๋ณต์‚ฌํ•˜๋ฉด ๋”ธ๋ ค ์˜ค๋Š” ํ—ค๋”๋“ค์ด๋‹ค.

ํ—ค๋” ์ƒํƒœ ํ•  ์ผ
X-XSS-Protection ๋ธŒ๋ผ์šฐ์ € XSS ํ•„ํ„ฐ๋Š” ์ œ๊ฑฐ๋จ. ํ•„ํ„ฐ ์ž์ฒด๊ฐ€ ์ •๋ณด ์œ ์ถœ์— ์•…์šฉ๋œ ์‚ฌ๋ก€๊ฐ€ ์žˆ์Œ ๋ณด๋‚ด์ง€ ์•Š๊ฑฐ๋‚˜ 0owasp-shp
Public-Key-Pins (HPKP) ์„ค์ •์„ ์ž˜๋ชปํ•˜๋ฉด ์‚ฌ์ดํŠธ๊ฐ€ ์žฅ๊ธฐ๊ฐ„ ์—ด๋ฆฌ์ง€ ์•Š๋Š” ๋ฌธ์ œ๋กœ ํ๊ธฐ ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค
Expect-CT Certificate Transparency๊ฐ€ ๊ธฐ๋ณธ ์š”๊ตฌ์‚ฌํ•ญ์ด ๋˜๋ฉด์„œ ํ๊ธฐ ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค

ํ—ค๋”๋ฅผ ๋ชป ๊ฑฐ๋Š” ํ˜ธ์ŠคํŒ…

GitHub Pages๋Š” ์‘๋‹ต ํ—ค๋” ์„ค์ • ๊ธฐ๋Šฅ์ด ์—†๋‹ค. ์ด ๋ธ”๋กœ๊ทธ ์‘๋‹ต์— HSTS๋งŒ ์žˆ๋Š” ์ด์œ ๋‹ค. ๋‚จ๋Š” ์ˆ˜๋‹จ์€ HTML ์•ˆ์˜ <meta http-equiv>์ธ๋ฐ, ํ—ค๋”์™€ ๋˜‘๊ฐ™์ด ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค.

ํ•ญ๋ชฉ <meta http-equiv>๋กœ ๋น„๊ณ 
CSP ์ถœ์ฒ˜ ๋ชฉ๋ก (script-src ๋“ฑ) โœ… <meta> ํƒœ๊ทธ๋ณด๋‹ค ์•ž์— ๋‚˜์˜จ ์š”์†Œ์—๋Š” ์ ์šฉ ์•ˆ ๋จ. <head> ๋งจ ์œ„์— ๋‘”๋‹ค
CSP frame-ancestors โŒ meta์—์„œ๋Š” ๋ฌด์‹œ๋œ๋‹คcsp3-meta
CSP report-uri, sandbox โŒ meta์—์„œ๋Š” ๋ฌด์‹œ๋œ๋‹ค
X-Frame-Options โŒ meta๋กœ ๋„ฃ์œผ๋ฉด ๋ฌด์‹œ๋œ๋‹ค
Referrer-Policy โœ… <meta name="referrer" content="...">
HSTS, nosniff, Permissions-Policy โŒ ํ—ค๋”๋กœ๋งŒ ๋™์ž‘

์ฆ‰ GitHub Pages์—์„œ๋Š” ํด๋ฆญ์žฌํ‚น ๋ฐฉ์–ด๋ฅผ ๊ฑธ ๋ฐฉ๋ฒ•์ด ์—†๋‹ค. ์ด ๋ธ”๋กœ๊ทธ๋Š” ๋กœ๊ทธ์ธ๋„ ๋ฒ„ํŠผ ๋™์ž‘๋„ ์—†๋Š” ์ฝ๊ธฐ ์ „์šฉ ํŽ˜์ด์ง€๋ผ ํด๋ฆญ์žฌํ‚น์œผ๋กœ ์žƒ์„ ๊ฒƒ์ด ๊ฑฐ์˜ ์—†์ง€๋งŒ, ๋กœ๊ทธ์ธ์ด ์žˆ๋Š” ์„œ๋น„์Šค๋ผ๋ฉด GitHub Pages๋Š” ๋งž์ง€ ์•Š๋Š”๋‹ค.

ํ—ค๋”๊ฐ€ ํ•„์š”ํ•˜๋ฉด ๊ณ ๋ฅผ ์ˆ˜ ์žˆ๋Š” ๊ธธ์€ ์…‹์ด๋‹ค.

๋ฐฉ๋ฒ• ์„ค์ • ์œ„์น˜
ํ—ค๋” ์„ค์ •์„ ์ง€์›ํ•˜๋Š” ์ •์  ํ˜ธ์ŠคํŒ… Netlify _headers ํŒŒ์ผ, Vercel vercel.json์˜ headers, Cloudflare Pages _headers
GitHub Pages ์•ž์— CDN Cloudflare ์‘๋‹ต ํ—ค๋” ๋ณ€ํ™˜ ๊ทœ์น™ (์‚ฌ์šฉ์ž ๋„๋ฉ”์ธ ํ•„์š”)
์„œ๋ฒ„ ์ง์ ‘ ์šด์˜ nginx, Express, Spring ์„ค์ •

์„œ๋ฒ„๋ณ„ ์„ค์ • ์˜ˆ

nginx

server {
    listen 443 ssl;
    # always๊ฐ€ ์—†์œผ๋ฉด 4xx, 5xx ์‘๋‹ต์—๋Š” ํ—ค๋”๊ฐ€ ์•ˆ ๋ถ™๋Š”๋‹ค
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location /api/ {
        # โŒ ์—ฌ๊ธฐ์„œ add_header๋ฅผ ํ•˜๋‚˜๋ผ๋„ ์“ฐ๋ฉด ์œ„ server ๋ธ”๋ก์˜ 4๊ฐœ๊ฐ€ ์ด location์—์„œ ์ „๋ถ€ ๋น ์ง„๋‹ค
        add_header Cache-Control "no-store";
        proxy_pass http://backend;
    }
}

โš ๏ธ nginx์˜ add_header๋Š” ํ•˜์œ„ ๋ธ”๋ก์— add_header๊ฐ€ ํ•˜๋‚˜๋„ ์—†์„ ๋•Œ๋งŒ ์ƒ์œ„ ๊ฐ’์„ ๋ฌผ๋ ค๋ฐ›๋Š”๋‹คnginx-add-header. location์— ์บ์‹œ ํ—ค๋” ํ•˜๋‚˜๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค๊ฐ€ ๋ณด์•ˆ ํ—ค๋”๊ฐ€ ๊ทธ ๊ฒฝ๋กœ์—์„œ๋งŒ ์‚ฌ๋ผ์ง€๋Š” ์ผ์ด ํ”ํ•˜๋‹ค. ๊ณตํ†ต ํ—ค๋”๋ฅผ ํŒŒ์ผ ํ•˜๋‚˜๋กœ ๋นผ๊ณ  include๋กœ ๊ฐ ๋ธ”๋ก์— ๋‹ค์‹œ ๋„ฃ๊ฑฐ๋‚˜, 1.29.3 ์ด์ƒ์ด๋ฉด add_header_inherit ์ง€์‹œ์–ด๋ฅผ ์“ด๋‹ค.

Express

const helmet = require("helmet");

// โœ… ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ CSP, HSTS, nosniff, frameguard ๋“ฑ์ด ํ•œ ๋ฒˆ์— ๋ถ™๋Š”๋‹ค
app.use(helmet());

Vercel

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
      ]
    }
  ]
}

Spring Security๋Š” ๊ธฐ๋ณธ์œผ๋กœ ์—ฌ๋Ÿฌ ํ—ค๋”๋ฅผ ๋ถ™์ธ๋‹ค. ์„ค์ • ๋ฐฉ๋ฒ•์€ Spring Security ์ธ์ฆ ๊ตฌ์กฐ ๊ธ€์— ์ •๋ฆฌํ–ˆ๋‹ค.

์ ๊ฒ€ ์Šคํฌ๋ฆฝํŠธ

curl -sI ๊ฒฐ๊ณผ๋ฅผ ๋ˆˆ์œผ๋กœ ๋ณด๋Š” ๊ฒƒ๋„ ๋˜์ง€๋งŒ, ๊ธฐ์ค€์„ ์ฝ”๋“œ๋กœ ์ ์–ด ๋‘๋ฉด ๋ฐฐํฌ ํ›„ ํ™•์ธ์— ๋‹ค์‹œ ์“ธ ์ˆ˜ ์žˆ๋‹ค. ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ ์“ด๋‹ค.

"""์‘๋‹ต ํ—ค๋” ์ ๊ฒ€๊ธฐ. ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ ์“ด๋‹ค."""
import re
import sys
import urllib.request


def hsts_ok(v):
    m = re.search(r"max-age=(\d+)", v or "")
    return bool(m) and int(m.group(1)) >= 31536000  # 1๋…„ ์ด์ƒ


def csp_ok(v):
    if not v:
        return False
    m = re.search(r"script-src([^;]*)", v)
    # script-src์— 'unsafe-inline'์ด ์žˆ์œผ๋ฉด XSS ๋ฐฉ์–ด๊ฐ€ ๊ฑฐ์˜ ์‚ฌ๋ผ์ง„๋‹ค
    return not (m and "'unsafe-inline'" in m.group(1))


RULES = [
    ("Strict-Transport-Security", hsts_ok, "max-age 1๋…„ ์ด์ƒ"),
    ("Content-Security-Policy", csp_ok, "script-src์— 'unsafe-inline' ์—†์Œ"),
    ("X-Frame-Options", lambda v: (v or "").upper() in ("DENY", "SAMEORIGIN"), "DENY/SAMEORIGIN"),
    ("X-Content-Type-Options", lambda v: (v or "").lower() == "nosniff", "nosniff"),
    ("Referrer-Policy", bool, "์žˆ์Œ"),
    ("Permissions-Policy", bool, "์žˆ์Œ"),
]


def check(url):
    req = urllib.request.Request(url, headers={"User-Agent": "header-check"})
    with urllib.request.urlopen(req, timeout=10) as res:
        headers = res.headers

    csp = headers.get("Content-Security-Policy") or ""
    for name, ok, rule in RULES:
        value = headers.get(name)
        passed = ok(value)
        # CSP frame-ancestors๊ฐ€ X-Frame-Options๋ฅผ ๋Œ€์‹ ํ•œ๋‹ค
        if name == "X-Frame-Options" and "frame-ancestors" in csp:
            passed = True
        print(f"{'PASS' if passed else 'FAIL'}  {name:<26} {(value or '(์—†์Œ)')[:40]:<42} ๊ธฐ์ค€: {rule}")

    for cookie in headers.get_all("Set-Cookie") or []:
        attrs = cookie.lower()
        missing = [a for a in ("secure", "httponly", "samesite") if a not in attrs]
        name = cookie.split("=", 1)[0]
        print(f"{'WARN' if missing else 'PASS'}  cookie {name:<19} ๋น ์ง„ ์†์„ฑ: {', '.join(missing) or '์—†์Œ'}")


if __name__ == "__main__":
    check(sys.argv[1])

๋‘ ์‚ฌ์ดํŠธ์— ๋Œ๋ฆฐ ๊ฒฐ๊ณผ๋‹ค.

$ python check_headers.py https://leekh8.github.io/
PASS  Strict-Transport-Security  max-age=31556952                           ๊ธฐ์ค€: max-age 1๋…„ ์ด์ƒ
FAIL  Content-Security-Policy    (์—†์Œ)                                     ๊ธฐ์ค€: script-src์— 'unsafe-inline' ์—†์Œ
FAIL  X-Frame-Options            (์—†์Œ)                                     ๊ธฐ์ค€: DENY/SAMEORIGIN
FAIL  X-Content-Type-Options     (์—†์Œ)                                     ๊ธฐ์ค€: nosniff
FAIL  Referrer-Policy            (์—†์Œ)                                     ๊ธฐ์ค€: ์žˆ์Œ
FAIL  Permissions-Policy         (์—†์Œ)                                     ๊ธฐ์ค€: ์žˆ์Œ

$ python check_headers.py https://github.com/
PASS  Strict-Transport-Security  max-age=31536000; includeSubdomains; pre   ๊ธฐ์ค€: max-age 1๋…„ ์ด์ƒ
PASS  Content-Security-Policy    default-src 'none'; base-uri 'self'; chi   ๊ธฐ์ค€: script-src์— 'unsafe-inline' ์—†์Œ
PASS  X-Frame-Options            deny                                       ๊ธฐ์ค€: DENY/SAMEORIGIN
PASS  X-Content-Type-Options     nosniff                                    ๊ธฐ์ค€: nosniff
PASS  Referrer-Policy            origin-when-cross-origin, strict-origin-   ๊ธฐ์ค€: ์žˆ์Œ
FAIL  Permissions-Policy         (์—†์Œ)                                     ๊ธฐ์ค€: ์žˆ์Œ
PASS  cookie _gh_sess            ๋น ์ง„ ์†์„ฑ: ์—†์Œ
WARN  cookie _octo               ๋น ์ง„ ์†์„ฑ: httponly
PASS  cookie logged_in           ๋น ์ง„ ์†์„ฑ: ์—†์Œ

์ด ์Šคํฌ๋ฆฝํŠธ๋Š” ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฅผ ๋”ฐ๋ผ๊ฐ„ ๋งˆ์ง€๋ง‰ ์‘๋‹ต๋งŒ ๋ณธ๋‹ค. http์—์„œ https๋กœ ๋„˜์–ด๊ฐ€๋Š” ์ฒซ ์‘๋‹ต, ๋กœ๊ทธ์ธ ํ›„ ํŽ˜์ด์ง€, API ๊ฒฝ๋กœ์ฒ˜๋Ÿผ ํ—ค๋”๊ฐ€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋Š” ์ฃผ์†Œ๋Š” ๋”ฐ๋กœ ๋„ฃ์–ด ๋Œ๋ฆฐ๋‹ค. nginx ์ƒ์† ๋ฌธ์ œ๋Š” /api/ ๊ฐ™์€ ํ•˜์œ„ ๊ฒฝ๋กœ๋ฅผ ์ฐ์–ด์•ผ ๋“œ๋Ÿฌ๋‚œ๋‹ค.

์ •๋ฆฌ

  1. ๋ณด์•ˆ ํ—ค๋”๋Š” ์ทจ์•ฝ์ ์„ ๊ณ ์น˜์ง€ ์•Š๋Š”๋‹ค. ์ทจ์•ฝ์ ์ด ์žˆ์„ ๋•Œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ณต๊ฒฉ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ฒŒ ๋ง‰๋Š” ๋งˆ์ง€๋ง‰ ์ธต์ด๋‹ค
  2. HSTS๋Š” ์ฒซ ์ ‘์†์„ ๋ชป ๋ง‰๋Š”๋‹ค. ์ฒซ ์ ‘์†๊นŒ์ง€ ๋ง‰์œผ๋ ค๋ฉด preload ๋ชฉ๋ก์— ๋„ฃ์–ด์•ผ ํ•˜๊ณ , includeSubDomains๋Š” ํ•˜์œ„ ๋„๋ฉ”์ธ์„ ์ „๋ถ€ ํ™•์ธํ•œ ๋’ค์— ๋ถ™์ธ๋‹ค
  3. script-src์— 'unsafe-inline'์„ ๋„ฃ์€ CSP๋Š” XSS๋ฅผ ๊ฑฐ์˜ ๋ง‰์ง€ ๋ชปํ•œ๋‹ค. ์ •์  ์‚ฌ์ดํŠธ๋Š” hash, ์„œ๋ฒ„ ๋ Œ๋”๋ง์€ nonce๋ฅผ ์“ฐ๊ณ , Report-Only๋กœ ๋จผ์ € ๊ฑด๋‹ค
  4. <meta>๋กœ ๊ฑฐ๋Š” CSP๋Š” ํ—ค๋”์™€ ๋‹ค๋ฅด๋‹ค. frame-ancestors๊ฐ€ ๋ฌด์‹œ๋˜๋ฏ€๋กœ ํด๋ฆญ์žฌํ‚น์€ ๋ง‰์„ ์ˆ˜ ์—†๋‹ค
  5. nginx๋Š” ํ•˜์œ„ ๋ธ”๋ก์— add_header๋ฅผ ํ•˜๋‚˜๋งŒ ์จ๋„ ์ƒ์œ„ ํ—ค๋”๊ฐ€ ๋ชจ๋‘ ๋น ์ง„๋‹ค. ํ•˜์œ„ ๊ฒฝ๋กœ๊นŒ์ง€ ์ฐ์–ด ๋ด์•ผ ๋ณด์ธ๋‹ค

์‹ค๋ฌด ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • curl -sI๋กœ ์ฒซ ํŽ˜์ด์ง€, ๋กœ๊ทธ์ธ ํ›„ ํŽ˜์ด์ง€, API ๊ฒฝ๋กœ, ์˜ค๋ฅ˜ ํŽ˜์ด์ง€(404)์˜ ํ—ค๋”๋ฅผ ๊ฐ๊ฐ ํ™•์ธํ•œ๋‹ค
  • HSTS max-age๋ฅผ ์งง๊ฒŒ ๊ฑธ์–ด ํ™•์ธํ•œ ๋’ค 1๋…„์œผ๋กœ ๋Š˜๋ฆฐ๋‹ค
  • includeSubDomains ์ „์— ํ•˜์œ„ ๋„๋ฉ”์ธ ๋ชฉ๋ก์„ ๋ฝ‘์•„ ์ „๋ถ€ https์ธ์ง€ ํ™•์ธํ•œ๋‹ค
  • CSP๋Š” Content-Security-Policy-Report-Only๋กœ ๋จผ์ € ๊ฑด๋‹ค
  • script-src์— 'unsafe-inline'๊ณผ 'unsafe-eval'์ด ์—†๋Š”์ง€ ๋ณธ๋‹ค
  • object-src 'none', base-uri 'self'๋ฅผ ๋„ฃ๋Š”๋‹ค. ๊ฑฐ์˜ ์•„๋ฌด๊ฒƒ๋„ ๊นจ์ง€ ์•Š๋Š”๋‹ค
  • ํด๋ฆญ์žฌํ‚น ๋ฐฉ์–ด๋กœ X-Frame-Options: DENY์™€ frame-ancestors 'none'์„ ๊ฐ™์ด ๊ฑด๋‹ค
  • X-Content-Type-Options: nosniff๋ฅผ ๋ชจ๋“  ์‘๋‹ต์— ๋ถ™์ธ๋‹ค
  • ์„ธ์…˜ ์ฟ ํ‚ค์— Secure, HttpOnly, SameSite๋ฅผ ๋ถ™์ด๊ณ , ๊ฐ€๋Šฅํ•˜๋ฉด __Host- ์ ‘๋‘์‚ฌ๋ฅผ ์“ด๋‹ค
  • ์˜ค๋ž˜๋œ ์˜ˆ์ œ์—์„œ ๋”ธ๋ ค ์˜จ X-XSS-Protection: 1, Public-Key-Pins, Expect-CT๋ฅผ ์ง€์šด๋‹ค
  • nginx์—์„œ location ๋ธ”๋ก์— add_header๋ฅผ ์“ฐ๋ฉด ๊ณตํ†ต ํ—ค๋”๋„ ๊ฑฐ๊ธฐ ๋‹ค์‹œ ๋„ฃ๋Š”๋‹ค
  • ์ •์  ํ˜ธ์ŠคํŒ…์ด ํ—ค๋” ์„ค์ •์„ ์ง€์›ํ•˜๋Š”์ง€ ๋จผ์ € ํ™•์ธํ•œ๋‹ค. ๋กœ๊ทธ์ธ์ด ์žˆ๋Š” ์„œ๋น„์Šค๋Š” ์ง€์›ํ•˜๋Š” ๊ณณ์œผ๋กœ ์˜ฎ๊ธด๋‹ค

๋‹ค์Œ ํŽธ์—์„œ๋Š” CSP๋ฅผ Report-Only๋กœ ๊ฑธ๊ณ  ์œ„๋ฐ˜ ๋ณด๊ณ ๋ฅผ ๋ชจ์•„ ์ •์ฑ…์„ ์ขํ˜€ ๊ฐ€๋Š” ๊ณผ์ •์„ ๋‹ค๋ฃฌ๋‹ค. ์ธ๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ hash๋ฅผ ๋นŒ๋“œ ๋‹จ๊ณ„์—์„œ ๊ณ„์‚ฐํ•˜๋Š” ๋ฐฉ๋ฒ•, ๊ด‘๊ณ ์™€ ๋ถ„์„ ์Šคํฌ๋ฆฝํŠธ๋ฅผ 'strict-dynamic'์œผ๋กœ ๋‹ค๋ฃจ๋Š” ๋ฐฉ๋ฒ•, ์œ„๋ฐ˜ ๋ณด๊ณ  ์ค‘ ๋ธŒ๋ผ์šฐ์ € ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์ด ๋งŒ๋“œ๋Š” ์žก์Œ์„ ๊ฑธ๋Ÿฌ๋‚ด๋Š” ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•œ๋‹ค.

์ฐธ๊ณ ๋ฌธํ—Œ


์ด์–ด์„œ ์ฝ๊ธฐ


  1. OWASP. "OWASP Secure Headers Project" (ํ—ค๋”๋ณ„ ๊ถŒ์žฅ๊ฐ’, X-XSS-Protection์€ ๋ณด๋‚ด์ง€ ์•Š๊ฑฐ๋‚˜ 0). https://owasp.org/www-project-secure-headers/โ†ฉ
  2. MDN Web Docs. "HTTP headers". https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headersโ†ฉ
  3. IETF. "RFC 6797: HTTP Strict Transport Security (HSTS)". https://www.rfc-editor.org/rfc/rfc6797โ†ฉ
  4. HSTS Preload List Submission (๋“ฑ๋ก ์กฐ๊ฑด). https://hstspreload.org/โ†ฉ
  5. MDN Web Docs. "Content Security Policy (CSP)". https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSPโ†ฉ
  6. web.dev. "Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)" (nonce ๋ฐฉ์‹๊ณผ hash ๋ฐฉ์‹, 'strict-dynamic'). https://web.dev/articles/strict-cspโ†ฉ
  7. MDN Web Docs. "X-Frame-Options" (frame-ancestors๊ฐ€ ์ด ํ—ค๋”๋ฅผ ๋Œ€์ฒดํ•œ๋‹ค). https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Frame-Optionsโ†ฉ
  8. MDN Web Docs. "Referrer-Policy" (๊ธฐ๋ณธ๊ฐ’ strict-origin-when-cross-origin). https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policyโ†ฉ
  9. W3C. "Content Security Policy Level 3 - Delivering a policy via the meta element" (frame-ancestors, report-uri, sandbox๋Š” meta์—์„œ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค). https://www.w3.org/TR/CSP3/#meta-elementโ†ฉ
  10. nginx. "Module ngx_http_headers_module - add_header" (ํ˜„์žฌ ๋‹จ๊ณ„์— add_header๊ฐ€ ์—†์„ ๋•Œ๋งŒ ์ƒ์œ„ ๋‹จ๊ณ„ ๊ฐ’์„ ๋ฌผ๋ ค๋ฐ›๋Š”๋‹ค). https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_headerโ†ฉ

@leekh8
๋ณด์•ˆ, ์›น ๊ฐœ๋ฐœ, Python์„ ๋‹ค๋ฃจ๋Š” ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ