์น ๋ณด์ ๊ธฐ์ด ์๋ฆฌ์ฆ
- [์ ๋ฌธ] ์น์ฌ์ดํธ ๊ธฐ์ ์คํ ํ์ง์ ํ๊ฑฐํ๋ฆฐํ ๋ณด์
- [1ํธ] OWASP Top 10:2025 ์์ ๊ฐ์ด๋
- [2ํธ] OWASP API Security Top 10:2023
- [3ํธ] OWASP LLM Top 10:2025
- [4ํธ] JWT, OAuth, ์ธ์ ๋ณด์ ์์ ๊ฐ์ด๋
- [5ํธ] HTTP ๋ณด์ ํค๋ ์ ๊ฒ โ ํ์ฌ ๊ธ
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๋ ์๋ต ํค๋๋ฅผ ์ฌ์ฉ์๊ฐ ๋ฐ๊ฟ ์ ์๋ค. ๊ทธ๋์ ์ด ๊ธ์ ํค๋๋ง๋ค ๋ฌด์์ ๋ง๋์ง ์ ๋ฆฌํ๊ณ , ํค๋๋ฅผ ๋ชป ๊ฑฐ๋ ํ๊ฒฝ์์ ์ด๋๊น์ง ๋ฉ์ธ ์ ์๋์ง๋ฅผ ๋ฐ๋ก ๋ค๋ฃฌ๋ค.
๋ณด์ ํค๋๊ฐ ํ๋ ์ผ
๋ณด์ ํค๋๋ ์๋ฒ๊ฐ ๋ธ๋ผ์ฐ์ ์๊ฒ "์ด ํ์ด์ง๋ ์ด๋ ๊ฒ ๋ค๋ค๋ผ"๋ผ๊ณ ์ง์ํ๋ ๊ฐ์ด๋ค. ์๋ฒ ์ฝ๋์ ์ทจ์ฝ์ ์ ๊ณ ์น์ง๋ ์๋๋ค. ์ทจ์ฝ์ ์ด ์์ด๋ ๋ธ๋ผ์ฐ์ ๊ฐ ๊ณต๊ฒฉ ์ฝ๋๋ฅผ ์คํํ์ง ์๊ฒ ๋ง๋ ๋ง์ง๋ง ์ธต์ด๋ค.
| ํค๋ | ๋ง๋ ๊ณต๊ฒฉ | ๊ถ์ฅ๊ฐ | ์ด ๋ธ๋ก๊ทธ |
|---|---|---|---|
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๋ก ๋๊ฐ๋ค.
์ด ์ฒซ ์ ์์ ๋ง๋ ๋ฐฉ๋ฒ์ด preload ๋ชฉ๋ก์ด๋ค. ๋ธ๋ผ์ฐ์ ๋ฐฐํฌํ ์์ "์ด ๋๋ฉ์ธ์ ์ฒ์๋ถํฐ https" ๋ชฉ๋ก์ ๋ฃ์ด ๋๋ค. ๋ฑ๋ก ์กฐ๊ฑด์ hstspreload.org์ ์ ํ ์๋คhstspreload.
- http ์์ฒญ์ ๊ฐ์ ํธ์คํธ์ https๋ก ๋ฆฌ๋ค์ด๋ ํธ
- ๋ชจ๋ ํ์ ๋๋ฉ์ธ์ด https ์ ๊ณต
- ๊ธฐ๋ณธ ๋๋ฉ์ธ ์๋ต์
max-age31536000 ์ด์,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๋ฅผ ์ฒ์ ๊ฑฐ๋ ์์๋ ์ด๋ ๋ค.
Content-Security-Policy-Report-Only๋ก ๊ฑด๋ค. ๋ง์ง ์๊ณ ์๋ฐ๋ง ๋ณด๊ณ ํ๋ค- ๋ธ๋ผ์ฐ์ ์ฝ์๊ณผ ๋ณด๊ณ ์์ง ์ฃผ์์ ์์ด๋ ์๋ฐ์ ๋ณด๊ณ ๋ชฉ๋ก์ ๊ณ ์น๋ค
- ์๋ฐ์ด ์ ์ ๋์์์ ๋ ์ ๋์ค๋ฉด
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/ ๊ฐ์ ํ์ ๊ฒฝ๋ก๋ฅผ ์ฐ์ด์ผ ๋๋ฌ๋๋ค.
์ ๋ฆฌ
- ๋ณด์ ํค๋๋ ์ทจ์ฝ์ ์ ๊ณ ์น์ง ์๋๋ค. ์ทจ์ฝ์ ์ด ์์ ๋ ๋ธ๋ผ์ฐ์ ๊ฐ ๊ณต๊ฒฉ ์ฝ๋๋ฅผ ์คํํ์ง ์๊ฒ ๋ง๋ ๋ง์ง๋ง ์ธต์ด๋ค
- HSTS๋ ์ฒซ ์ ์์ ๋ชป ๋ง๋๋ค. ์ฒซ ์ ์๊น์ง ๋ง์ผ๋ ค๋ฉด preload ๋ชฉ๋ก์ ๋ฃ์ด์ผ ํ๊ณ ,
includeSubDomains๋ ํ์ ๋๋ฉ์ธ์ ์ ๋ถ ํ์ธํ ๋ค์ ๋ถ์ธ๋ค script-src์'unsafe-inline'์ ๋ฃ์ CSP๋ XSS๋ฅผ ๊ฑฐ์ ๋ง์ง ๋ชปํ๋ค. ์ ์ ์ฌ์ดํธ๋ hash, ์๋ฒ ๋ ๋๋ง์ nonce๋ฅผ ์ฐ๊ณ , Report-Only๋ก ๋จผ์ ๊ฑด๋ค<meta>๋ก ๊ฑฐ๋ CSP๋ ํค๋์ ๋ค๋ฅด๋ค.frame-ancestors๊ฐ ๋ฌด์๋๋ฏ๋ก ํด๋ฆญ์ฌํน์ ๋ง์ ์ ์๋ค- 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'์ผ๋ก ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ, ์๋ฐ ๋ณด๊ณ ์ค ๋ธ๋ผ์ฐ์ ํ์ฅ ํ๋ก๊ทธ๋จ์ด ๋ง๋๋ ์ก์์ ๊ฑธ๋ฌ๋ด๋ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํ๋ค.
์ฐธ๊ณ ๋ฌธํ
์ด์ด์ ์ฝ๊ธฐ
- OWASP Top 10:2025 โ A02 ๋ณด์ ์ค์ ์ค๋ฅ
- JWT, OAuth, ์ธ์ ๋ณด์ ์์ ๊ฐ์ด๋
- ์น์ฌ์ดํธ ๊ธฐ์ ์คํ ํ์ง์ ํ๊ฑฐํ๋ฆฐํ ๋ณด์
- OWASP. "OWASP Secure Headers Project" (ํค๋๋ณ ๊ถ์ฅ๊ฐ,
X-XSS-Protection์ ๋ณด๋ด์ง ์๊ฑฐ๋ 0). https://owasp.org/www-project-secure-headers/โฉ - MDN Web Docs. "HTTP headers". https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headersโฉ
- IETF. "RFC 6797: HTTP Strict Transport Security (HSTS)". https://www.rfc-editor.org/rfc/rfc6797โฉ
- HSTS Preload List Submission (๋ฑ๋ก ์กฐ๊ฑด). https://hstspreload.org/โฉ
- MDN Web Docs. "Content Security Policy (CSP)". https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSPโฉ
- web.dev. "Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)" (nonce ๋ฐฉ์๊ณผ hash ๋ฐฉ์,
'strict-dynamic'). https://web.dev/articles/strict-cspโฉ - MDN Web Docs. "X-Frame-Options" (
frame-ancestors๊ฐ ์ด ํค๋๋ฅผ ๋์ฒดํ๋ค). https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Frame-Optionsโฉ - MDN Web Docs. "Referrer-Policy" (๊ธฐ๋ณธ๊ฐ
strict-origin-when-cross-origin). https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policyโฉ - 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โฉ - nginx. "Module ngx_http_headers_module - add_header" (ํ์ฌ ๋จ๊ณ์
add_header๊ฐ ์์ ๋๋ง ์์ ๋จ๊ณ ๊ฐ์ ๋ฌผ๋ ค๋ฐ๋๋ค). https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_headerโฉ




