์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ›ก๏ธ ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ ์ทจ์•ฝ์ ๊ณผ ์•ˆ์ „ํ•œ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•: ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ฐ˜๋“œ์‹œ ์•Œ์•„์•ผ ํ•  ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿ›ก๏ธ ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ ์ทจ์•ฝ์ ๊ณผ ์•ˆ์ „ํ•œ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•: ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ฐ˜๋“œ์‹œ ์•Œ์•„์•ผ ํ•  ์™„๋ฒฝ ๊ฐ€์ด๋“œ

์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ฐ€์žฅ ์œ„ํ—˜ํ•œ ๊ณต๊ฒฉ ๋ฒกํ„ฐ๋ฅผ ๋ง‰์•„๋‚ด๋Š” ์‹ค์ „ ๋ณด์•ˆ ์ „๋žต

๐ŸŽฏ ์ด ๊ธ€์„ ์ฝ์–ด์•ผ ํ•˜๋Š” ์ด์œ  ์นœ๊ตฌ์•ผ, ํ˜น์‹œ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•  ๋•Œ "๊ทธ๋ƒฅ ์—…๋กœ๋“œ๋งŒ ๋˜๋ฉด ๋˜์ง€ ๋ญ~" ์ด๋Ÿฐ ์ƒ๊ฐ ํ•ด๋ณธ ์  ์žˆ์–ด? ๐Ÿ˜… ์‚ฌ์‹ค ๋‚˜๋„ ์ฒ˜์Œ์—” ๊ทธ๋žฌ์–ด. ๊ทผ๋ฐ ๋ง์ด์•ผ, ํŒŒ์ผ ์—…๋กœ๋“œ๋Š” ํ•ด์ปค๋“ค์ด ๊ฐ€์žฅ ์ข‹์•„ํ•˜๋Š” ๊ณต๊ฒฉ ๊ฒฝ๋กœ ์ค‘ ํ•˜๋‚˜๋ผ๋Š” ๊ฑฐ ์•Œ๊ณ  ์žˆ์—ˆ์–ด? 2023๋…„ OWASP Top 10์—์„œ๋„ ์—ฌ์ „ํžˆ ์ƒ์œ„๊ถŒ์„ ์ฐจ์ง€ํ•˜๊ณ  ์žˆ์„ ์ •๋„๋กœ ์‹ฌ๊ฐํ•œ ๋ณด์•ˆ ์ด์Šˆ์•ผ.

์ด ๊ธ€์—์„œ๋Š” ํŒŒ์ผ ์—…๋กœ๋“œ์™€ ๊ด€๋ จ๋œ ๋ชจ๋“  ๋ณด์•ˆ ์ทจ์•ฝ์ ๊ณผ ๊ทธ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์นœ๊ตฌ์—๊ฒŒ ์„ค๋ช…ํ•˜๋“ฏ ์‰ฝ๊ณ  ์žฌ๋ฏธ์žˆ๊ฒŒ ํ’€์–ด๋ณผ ๊ฑฐ์•ผ. ์‹ค์ œ ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ๋ง์ด์ง€! ๐Ÿš€
์‚ฌ์šฉ์ž ๐Ÿ“ ์•…์„ฑํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ๋„ ์„œ๋ฒ„ ๋ณด์•ˆ ๊ฒ€์ฆ ํ™•์žฅ์ž / MIME / ํฌ๊ธฐ ๐Ÿ›ก๏ธ ์•ˆ์ „ํ•œ ์ €์žฅ์†Œ ์•ˆ์ „ํ•œ ์‹œ์Šคํ…œ โœ… ๊ฒ€์ฆ ์™„๋ฃŒ ์•ˆ์ „ํ•œ ํŒŒ์ผ๋งŒ ์ €์žฅ ๋ฐ ์„œ๋น„์Šค

๐Ÿ”ฅ ํŒŒ์ผ ์—…๋กœ๋“œ, ์™œ ์ด๋ ‡๊ฒŒ ์œ„ํ—˜ํ•œ ๊ฑธ๊นŒ?

์ž, ์ด์•ผ๊ธฐ๋ฅผ ํ•˜๋‚˜ ํ•ด์ค„๊ฒŒ. ๋‚ด ์นœ๊ตฌ๊ฐ€ ์ž‘์€ ์Šคํƒ€ํŠธ์—…์—์„œ ๊ฐœ๋ฐœ์ž๋กœ ์ผํ•˜๊ณ  ์žˆ์—ˆ์–ด. ์–ด๋А ๋‚  ํ”„๋กœํ•„ ์‚ฌ์ง„ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ๊ธ‰ํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์•ผ ํ–ˆ๋Š”๋ฐ, ์‹œ๊ฐ„์— ์ซ“๊ฒจ์„œ ๊ธฐ๋ณธ์ ์ธ ๊ฒ€์ฆ๋งŒ ํ•˜๊ณ  ๋ฐฐํฌํ–ˆ๋Œ€. ๊ทธ๋Ÿฐ๋ฐ ์ผ์ฃผ์ผ ๋’ค์— ์„œ๋ฒ„๊ฐ€ ๋จนํ†ต์ด ๋˜๋”๋ž˜. ๐Ÿ˜ฑ

์•Œ๊ณ  ๋ณด๋‹ˆ ๋ˆ„๊ตฐ๊ฐ€๊ฐ€ PHP ์›น์…ธ์„ ์ด๋ฏธ์ง€ ํŒŒ์ผ๋กœ ์œ„์žฅํ•ด์„œ ์—…๋กœ๋“œํ–ˆ๊ณ , ๊ทธ๊ฑธ ํ†ตํ•ด ์„œ๋ฒ„ ์ „์ฒด๋ฅผ ์žฅ์•…ํ•œ ๊ฑฐ์•ผ. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ •๋ณด๋Š” ๋ฌผ๋ก ์ด๊ณ  ๊ณ ๊ฐ ๊ฐœ์ธ์ •๋ณด๊นŒ์ง€ ์œ ์ถœ๋  ๋ป”ํ–ˆ์–ด. ๋‹คํ–‰ํžˆ ๋ฐฑ์—…์ด ์žˆ์–ด์„œ ๋ณต๊ตฌํ–ˆ์ง€๋งŒ, ๊ทธ ์นœ๊ตฌ๋Š” ๋ฉฐ์น  ๋™์•ˆ ์ž ๋„ ๋ชป ์ž๊ณ  ์‚ฌ๊ณ  ์ˆ˜์Šตํ•˜๋А๋ผ ๊ณ ์ƒํ–ˆ๋Œ€.

์ด๊ฒŒ ๋ฐ”๋กœ ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ์ด ์ค‘์š”ํ•œ ์ด์œ ์•ผ. ๋‹จ์ˆœํ•ด ๋ณด์ด๋Š” ๊ธฐ๋Šฅ์ด์ง€๋งŒ, ์ž˜๋ชป ๊ตฌํ˜„ํ•˜๋ฉด ์‹œ์Šคํ…œ ์ „์ฒด๊ฐ€ ์œ„ํ—˜์— ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ๊ฑฐ๋“ . ๐ŸŽฏ
โš ๏ธ ์‹ค์ œ ํ†ต๊ณ„๋กœ ๋ณด๋Š” ํŒŒ์ผ ์—…๋กœ๋“œ ๊ณต๊ฒฉ โ€ข 2022๋…„ ๊ธฐ์ค€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณต๊ฒฉ์˜ ์•ฝ 35%๊ฐ€ ํŒŒ์ผ ์—…๋กœ๋“œ ์ทจ์•ฝ์ ์„ ์ด์šฉ
โ€ข ํ‰๊ท  ํƒ์ง€ ์‹œ๊ฐ„: 197์ผ (์•ฝ 6๊ฐœ์›” ์ด์ƒ ํ•ด์ปค๊ฐ€ ์‹œ์Šคํ…œ ๋‚ด๋ถ€์— ์ž ๋ณต)
โ€ข ํ‰๊ท  ํ”ผํ•ด ๋ณต๊ตฌ ๋น„์šฉ: ์•ฝ 4์–ต 2์ฒœ๋งŒ ์›
โ€ข ๊ฐ€์žฅ ๋งŽ์ด ์—…๋กœ๋“œ๋˜๋Š” ์•…์„ฑ ํŒŒ์ผ: ์›น์…ธ (47%), ๋žœ์„ฌ์›จ์–ด (28%), ๋ฐฑ๋„์–ด (25%)

๐ŸŽญ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ณต๊ฒฉ์˜ ์ฃผ์š” ์œ ํ˜•๋“ค

ํŒŒ์ผ ์—…๋กœ๋“œ ๊ณต๊ฒฉ์€ ์ •๋ง ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๋กœ ๋‚˜ํƒ€๋‚˜. ๋งˆ์น˜ ๋ณ€์‹ ์˜ ๊ท€์žฌ ๊ฐ™๋‹ฌ๊นŒ? ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด์ž!

1๏ธโƒฃ ์›น์…ธ(Web Shell) ์—…๋กœ๋“œ ๊ณต๊ฒฉ

๐Ÿ’€ ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค

ํ•ด์ปค๊ฐ€ ์„œ๋ฒ„ ์ธก ์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ(PHP, JSP, ASP ๋“ฑ)์„ ์—…๋กœ๋“œํ•ด์„œ ์„œ๋ฒ„๋ฅผ ์›๊ฒฉ์œผ๋กœ ์ œ์–ดํ•˜๋Š” ๊ณต๊ฒฉ์ด์•ผ. ์ด๊ฒŒ ์„ฑ๊ณตํ•˜๋ฉด ํ•ด์ปค๋Š” ์‚ฌ์‹ค์ƒ ์„œ๋ฒ„์˜ ๊ด€๋ฆฌ์ž๊ฐ€ ๋˜๋Š” ๊ฑฐ์ง€.
์•…์„ฑ PHP ์›น์…ธ์˜ ์˜ˆ์‹œ๋ฅผ ๋ณด์—ฌ์ค„๊ฒŒ:
๐Ÿšจ ์•…์„ฑ ์›น์…ธ ์˜ˆ์ œ (์ ˆ๋Œ€ ์‚ฌ์šฉ ๊ธˆ์ง€!)
<?php
// ์ด๋ฏธ์ง€ ํŒŒ์ผ๋กœ ์œ„์žฅํ•œ ์›น์…ธ
// ํŒŒ์ผ๋ช…: innocent_image.php.jpg
system($_GET['cmd']); // ์™ธ๋ถ€์—์„œ ๋ช…๋ น์–ด ์‹คํ–‰ ๊ฐ€๋Šฅ
?>

// ๋˜๋Š” ๋” ๊ต๋ฌ˜ํ•œ ๋ฐฉ์‹
<?php
if(isset($_REQUEST['execute'])){
    eval($_REQUEST['execute']); // ์ž„์˜์˜ PHP ์ฝ”๋“œ ์‹คํ–‰
}
?>
์ด๋Ÿฐ ํŒŒ์ผ์ด ์—…๋กœ๋“œ๋˜๊ณ  ์‹คํ–‰๋˜๋ฉด, ํ•ด์ปค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ URL๋กœ ์„œ๋ฒ„๋ฅผ ๋งˆ์Œ๋Œ€๋กœ ์กฐ์ข…ํ•  ์ˆ˜ ์žˆ์–ด:
https://example.com/uploads/innocent_image.php.jpg?cmd=ls -la


๋ฌด์„ญ์ง€? ๐Ÿ˜จ ์„œ๋ฒ„์˜ ๋ชจ๋“  ํŒŒ์ผ์„ ๋ณผ ์ˆ˜ ์žˆ๊ณ , ์‚ญ์ œํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•  ์ˆ˜๋„ ์žˆ์–ด.

2๏ธโƒฃ ๋”๋ธ” ์ต์Šคํ…์…˜(Double Extension) ๊ณต๊ฒฉ

์ด๊ฑด ์ •๋ง ๊ต๋ฌ˜ํ•œ ๋ฐฉ๋ฒ•์ด์•ผ. ํŒŒ์ผ ์ด๋ฆ„์„ photo.jpg.php ์ด๋Ÿฐ ์‹์œผ๋กœ ๋งŒ๋“œ๋Š” ๊ฑฐ์ง€. ์ผ๋ถ€ ์„œ๋ฒ„ ์„ค์ •์—์„œ๋Š” ๋งˆ์ง€๋ง‰ ํ™•์žฅ์ž๋งŒ ๋ณด๊ณ  ์‹คํ–‰ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ด๋ฏธ์ง€ ํŒŒ์ผ์ธ ์ฒ™ํ•˜๋ฉด์„œ ์‹ค์ œ๋กœ๋Š” PHP ์Šคํฌ๋ฆฝํŠธ๋กœ ์‹คํ–‰๋  ์ˆ˜ ์žˆ์–ด.
ํŒŒ์ผ๋ช… ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๋Š” ๊ฒƒ ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ ์œ„ํ—˜๋„
image.jpg.php JPG ์ด๋ฏธ์ง€ PHP ์Šคํฌ๋ฆฝํŠธ ๐Ÿ”ด ๋งค์šฐ ๋†’์Œ
document.pdf.jsp PDF ๋ฌธ์„œ JSP ์Šคํฌ๋ฆฝํŠธ ๐Ÿ”ด ๋งค์šฐ ๋†’์Œ
file.txt.asp ํ…์ŠคํŠธ ํŒŒ์ผ ASP ์Šคํฌ๋ฆฝํŠธ ๐Ÿ”ด ๋งค์šฐ ๋†’์Œ
normal.jpg JPG ์ด๋ฏธ์ง€ ์ด๋ฏธ์ง€ ๐ŸŸข ์•ˆ์ „

3๏ธโƒฃ MIME ํƒ€์ž… ์Šคํ‘ธํ•‘(Spoofing)

MIME ํƒ€์ž…์€ ํŒŒ์ผ์˜ ์ข…๋ฅ˜๋ฅผ ์•Œ๋ ค์ฃผ๋Š” ์ •๋ณด์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด image/jpeg, application/pdf ๊ฐ™์€ ๊ฑฐ์ง€. ๊ทผ๋ฐ ์ด๊ฑธ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒŒ ๋ฌธ์ œ์•ผ!

ํ•ด์ปค๋Š” ์•…์„ฑ PHP ํŒŒ์ผ์˜ MIME ํƒ€์ž…์„ image/jpeg๋กœ ์†์—ฌ์„œ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์–ด. ์„œ๋ฒ„๊ฐ€ MIME ํƒ€์ž…๋งŒ ๋ฏฟ๊ณ  ๊ฒ€์ฆํ•œ๋‹ค๋ฉด? ๋‹น์—ฐํžˆ ๋šซ๋ฆฌ๊ฒ ์ง€! ๐ŸŽช
๐Ÿ” MIME ํƒ€์ž… ์กฐ์ž‘ ์˜ˆ์‹œ
POST /upload HTTP/1.1
Host: vulnerable-site.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg  โ† ์‹ค์ œ๋กœ๋Š” PHP ํŒŒ์ผ์ธ๋ฐ ์ด๋ฏธ์ง€๋กœ ์†์ž„!

<?php system($_GET['cmd']); ?>
------WebKitFormBoundary--

4๏ธโƒฃ ํŒŒ์ผ ํฌ๊ธฐ ๊ณต๊ฒฉ (DoS)

์ด๊ฑด ์ข€ ๋‹ค๋ฅธ ๋ฐฉ์‹์ด์•ผ. ์—„์ฒญ๋‚˜๊ฒŒ ํฐ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•ด์„œ ์„œ๋ฒ„์˜ ๋””์Šคํฌ ๊ณต๊ฐ„์ด๋‚˜ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๊ณ ๊ฐˆ์‹œํ‚ค๋Š” ๊ฑฐ์ง€. ์˜ˆ๋ฅผ ๋“ค์–ด 10GB์งœ๋ฆฌ ํŒŒ์ผ์„ ๋™์‹œ์— 100๊ฐœ ์—…๋กœ๋“œํ•˜๋ฉด? ์„œ๋ฒ„๋Š” ๊ทธ๋ƒฅ ์ฃฝ์–ด๋ฒ„๋ ค. ๐Ÿ’€

์‹ค์ œ๋กœ 2021๋…„์— ํ•œ ์‡ผํ•‘๋ชฐ ์‚ฌ์ดํŠธ๊ฐ€ ์ด๋Ÿฐ ๊ณต๊ฒฉ์„ ๋ฐ›์•„์„œ ํ•˜๋ฃจ ์ข…์ผ ์„œ๋น„์Šค๊ฐ€ ๋งˆ๋น„๋œ ์ ์ด ์žˆ์–ด. ๋งค์ถœ ์†์‹ค๋งŒ ์ˆ˜์–ต ์›์ด์—ˆ๋Œ€.

5๏ธโƒฃ ๊ฒฝ๋กœ ์กฐ์ž‘(Path Traversal) ๊ณต๊ฒฉ

ํŒŒ์ผ๋ช…์— ../ ๊ฐ™์€ ๊ฒฝ๋กœ ๋ฌธ์ž๋ฅผ ๋„ฃ์–ด์„œ ์˜๋„ํ•˜์ง€ ์•Š์€ ๋””๋ ‰ํ† ๋ฆฌ์— ํŒŒ์ผ์„ ์ €์žฅํ•˜๋Š” ๊ณต๊ฒฉ์ด์•ผ.
๐ŸŽฏ ๊ฒฝ๋กœ ์กฐ์ž‘ ๊ณต๊ฒฉ ์˜ˆ์‹œ
// ๊ณต๊ฒฉ์ž๊ฐ€ ๋ณด๋‚ด๋Š” ํŒŒ์ผ๋ช…
filename = "../../../etc/passwd"

// ๋˜๋Š”
filename = "..\\..\\..\\windows\\system32\\config\\sam"

// ์„œ๋ฒ„๊ฐ€ ์ œ๋Œ€๋กœ ๊ฒ€์ฆํ•˜์ง€ ์•Š์œผ๋ฉด
// ์ค‘์š”ํ•œ ์‹œ์Šคํ…œ ํŒŒ์ผ์„ ๋ฎ์–ด์“ธ ์ˆ˜ ์žˆ์Œ!
์ƒ์ƒํ•ด๋ด. ์‹œ์Šคํ…œ์˜ ์ค‘์š”ํ•œ ์„ค์ • ํŒŒ์ผ์ด๋‚˜ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์˜ ํŒŒ์ผ์„ ๋ฎ์–ด์“ธ ์ˆ˜ ์žˆ๋‹ค๋ฉด? ๐Ÿ˜ฑ ์™„์ „ ์žฌ์•™์ด์ง€!

๐Ÿ›ก๏ธ ์•ˆ์ „ํ•œ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ตฌํ˜„ํ•˜๊ธฐ: ์‹ค์ „ ๊ฐ€์ด๋“œ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๋ฐฉ์–ด ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ์„ค๋ช…ํ• ๊ฒŒ. ๐Ÿ’ช

๐Ÿ“‹ 1๋‹จ๊ณ„: ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ธฐ๋ฐ˜ ํ™•์žฅ์ž ๊ฒ€์ฆ

๐Ÿ’ก ํ•ต์‹ฌ ์›์น™ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ๋ฐฉ์‹(๊ธˆ์ง€ํ•  ํ™•์žฅ์ž ๋‚˜์—ด)์ด ์•„๋‹ˆ๋ผ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๋ฐฉ์‹(ํ—ˆ์šฉํ•  ํ™•์žฅ์ž๋งŒ ๋ช…์‹œ)์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด! ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ๋Š” ์šฐํšŒ ๋ฐฉ๋ฒ•์ด ๋„ˆ๋ฌด ๋งŽ๊ฑฐ๋“ .
โœ… ์•ˆ์ „ํ•œ ํ™•์žฅ์ž ๊ฒ€์ฆ (Node.js/Express)
const path = require('path');
const multer = require('multer');

// ํ—ˆ์šฉํ•  ํ™•์žฅ์ž ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ
const ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.pdf'];
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, '/secure/uploads/') // ์›น ๋ฃจํŠธ ๋ฐ–์˜ ์•ˆ์ „ํ•œ ๊ฒฝ๋กœ
  },
  filename: function (req, file, cb) {
    // ์›๋ณธ ํŒŒ์ผ๋ช… ์‚ฌ์šฉ ๊ธˆ์ง€! ๋žœ๋ค ์ด๋ฆ„ ์ƒ์„ฑ
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    const ext = path.extname(file.originalname).toLowerCase();
    
    // ํ™•์žฅ์ž ๊ฒ€์ฆ
    if (!ALLOWED_EXTENSIONS.includes(ext)) {
      return cb(new Error('ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค.'));
    }
    
    cb(null, file.fieldname + '-' + uniqueSuffix + ext);
  }
});

const fileFilter = (req, file, cb) => {
  const ext = path.extname(file.originalname).toLowerCase();
  
  // ์ด์ค‘ ํ™•์žฅ์ž ์ฒดํฌ
  const nameWithoutExt = path.basename(file.originalname, ext);
  if (nameWithoutExt.includes('.')) {
    return cb(new Error('์ด์ค‘ ํ™•์žฅ์ž๋Š” ํ—ˆ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'), false);
  }
  
  if (ALLOWED_EXTENSIONS.includes(ext)) {
    cb(null, true);
  } else {
    cb(new Error('ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค.'), false);
  }
};

const upload = multer({ 
  storage: storage,
  fileFilter: fileFilter,
  limits: { fileSize: MAX_FILE_SIZE }
});

// ๋ผ์šฐํŠธ์—์„œ ์‚ฌ์šฉ
app.post('/upload', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'ํŒŒ์ผ์ด ์—…๋กœ๋“œ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.' });
  }
  
  res.json({ 
    message: 'ํŒŒ์ผ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
    filename: req.file.filename 
  });
});
โœ… ์•ˆ์ „ํ•œ ํ™•์žฅ์ž ๊ฒ€์ฆ (Python/Flask)
import os
import uuid
from flask import Flask, request, jsonify
from werkzeug.utils import secure_filename

app = Flask(__name__)

# ํ—ˆ์šฉํ•  ํ™•์žฅ์ž ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'pdf'}
MAX_FILE_SIZE = 5 * 1024 * 1024  # 5MB
UPLOAD_FOLDER = '/secure/uploads/'

def allowed_file(filename):
    """์•ˆ์ „ํ•œ ํŒŒ์ผ ํ™•์žฅ์ž ๊ฒ€์ฆ"""
    # ํ™•์žฅ์ž๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ
    if '.' not in filename:
        return False
    
    # ์†Œ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ํ™•์žฅ์ž ์ถ”์ถœ
    ext = filename.rsplit('.', 1)[1].lower()
    
    # ์ด์ค‘ ํ™•์žฅ์ž ์ฒดํฌ
    name_without_ext = filename.rsplit('.', 1)[0]
    if '.' in name_without_ext:
        return False
    
    return ext in ALLOWED_EXTENSIONS

def generate_safe_filename(original_filename):
    """์•ˆ์ „ํ•œ ํŒŒ์ผ๋ช… ์ƒ์„ฑ"""
    ext = original_filename.rsplit('.', 1)[1].lower()
    # UUID๋กœ ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ์ด๋ฆ„ ์ƒ์„ฑ
    safe_name = f"{uuid.uuid4().hex}.{ext}"
    return safe_name

@app.route('/upload', methods=['POST'])
def upload_file():
    # ํŒŒ์ผ์ด ์š”์ฒญ์— ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ
    if 'file' not in request.files:
        return jsonify({'error': 'ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.'}), 400
    
    file = request.files['file']
    
    # ํŒŒ์ผ๋ช…์ด ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธ
    if file.filename == '':
        return jsonify({'error': 'ํŒŒ์ผ์ด ์„ ํƒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.'}), 400
    
    # ํŒŒ์ผ ํฌ๊ธฐ ๊ฒ€์ฆ
    file.seek(0, os.SEEK_END)
    file_size = file.tell()
    file.seek(0)
    
    if file_size > MAX_FILE_SIZE:
        return jsonify({'error': 'ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค.'}), 400
    
    # ํ™•์žฅ์ž ๊ฒ€์ฆ
    if not allowed_file(file.filename):
        return jsonify({'error': 'ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค.'}), 400
    
    # ์•ˆ์ „ํ•œ ํŒŒ์ผ๋ช… ์ƒ์„ฑ
    safe_filename = generate_safe_filename(file.filename)
    filepath = os.path.join(UPLOAD_FOLDER, safe_filename)
    
    # ํŒŒ์ผ ์ €์žฅ
    file.save(filepath)
    
    return jsonify({
        'message': 'ํŒŒ์ผ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
        'filename': safe_filename
    }), 200

๐Ÿ”ฌ 2๋‹จ๊ณ„: ๋งค์ง ๋ฐ”์ดํŠธ(Magic Byte) ๊ฒ€์ฆ

ํ™•์žฅ์ž๋งŒ ๋ฏฟ์œผ๋ฉด ์•ˆ ๋ผ! ํŒŒ์ผ์˜ ์‹ค์ œ ๋‚ด์šฉ์„ ํ™•์ธํ•ด์•ผ ํ•ด. ๋ชจ๋“  ํŒŒ์ผ ํ˜•์‹์€ ๊ณ ์œ ํ•œ "๋งค์ง ๋ฐ”์ดํŠธ"๋ผ๋Š” ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ฑฐ๋“ . ๐ŸŽฉโœจ
ํŒŒ์ผ ํ˜•์‹ ๋งค์ง ๋ฐ”์ดํŠธ (16์ง„์ˆ˜) ์„ค๋ช…
JPEG FF D8 FF ๋ชจ๋“  JPEG ํŒŒ์ผ์€ ์ด ๋ฐ”์ดํŠธ๋กœ ์‹œ์ž‘
PNG 89 50 4E 47 PNG ์‹œ๊ทธ๋‹ˆ์ฒ˜
GIF 47 49 46 38 "GIF8"์˜ ASCII ์ฝ”๋“œ
PDF 25 50 44 46 "%PDF"์˜ ASCII ์ฝ”๋“œ
ZIP 50 4B 03 04 ZIP ์•„์นด์ด๋ธŒ
๐Ÿ” ๋งค์ง ๋ฐ”์ดํŠธ ๊ฒ€์ฆ ๊ตฌํ˜„ (Node.js)
const fs = require('fs').promises;

// ํŒŒ์ผ ํ˜•์‹๋ณ„ ๋งค์ง ๋ฐ”์ดํŠธ ์ •์˜
const MAGIC_BYTES = {
  'jpg': [0xFF, 0xD8, 0xFF],
  'png': [0x89, 0x50, 0x4E, 0x47],
  'gif': [0x47, 0x49, 0x46, 0x38],
  'pdf': [0x25, 0x50, 0x44, 0x46],
};

async function verifyFileType(filePath, expectedExtension) {
  try {
    // ํŒŒ์ผ์˜ ์ฒซ ๋ถ€๋ถ„๋งŒ ์ฝ๊ธฐ (์„ฑ๋Šฅ ์ตœ์ ํ™”)
    const buffer = Buffer.alloc(8);
    const fd = await fs.open(filePath, 'r');
    await fd.read(buffer, 0, 8, 0);
    await fd.close();
    
    // ์˜ˆ์ƒ๋˜๋Š” ํ™•์žฅ์ž์˜ ๋งค์ง ๋ฐ”์ดํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
    const expectedBytes = MAGIC_BYTES[expectedExtension];
    
    if (!expectedBytes) {
      throw new Error('์ง€์›ํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค.');
    }
    
    // ๋งค์ง ๋ฐ”์ดํŠธ ๋น„๊ต
    for (let i = 0; i < expectedBytes.length; i++) {
      if (buffer[i] !== expectedBytes[i]) {
        return false;
      }
    }
    
    return true;
  } catch (error) {
    console.error('ํŒŒ์ผ ๊ฒ€์ฆ ์˜ค๋ฅ˜:', error);
    return false;
  }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
app.post('/upload', upload.single('file'), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.' });
  }
  
  const ext = path.extname(req.file.filename).slice(1).toLowerCase();
  const isValid = await verifyFileType(req.file.path, ext);
  
  if (!isValid) {
    // ํŒŒ์ผ ์‚ญ์ œ
    await fs.unlink(req.file.path);
    return res.status(400).json({ 
      error: 'ํŒŒ์ผ ํ˜•์‹์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์œ„์กฐ๋œ ํŒŒ์ผ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.' 
    });
  }
  
  res.json({ 
    message: 'ํŒŒ์ผ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
    filename: req.file.filename 
  });
});
๐Ÿ” ๋งค์ง ๋ฐ”์ดํŠธ ๊ฒ€์ฆ ๊ตฌํ˜„ (Python)
import os

# ํŒŒ์ผ ํ˜•์‹๋ณ„ ๋งค์ง ๋ฐ”์ดํŠธ ์ •์˜
MAGIC_BYTES = {
    'jpg': bytes([0xFF, 0xD8, 0xFF]),
    'jpeg': bytes([0xFF, 0xD8, 0xFF]),
    'png': bytes([0x89, 0x50, 0x4E, 0x47]),
    'gif': bytes([0x47, 0x49, 0x46, 0x38]),
    'pdf': bytes([0x25, 0x50, 0x44, 0x46]),
}

def verify_file_type(file_path, expected_extension):
    """ํŒŒ์ผ์˜ ๋งค์ง ๋ฐ”์ดํŠธ๋ฅผ ๊ฒ€์ฆ"""
    try:
        with open(file_path, 'rb') as f:
            # ํŒŒ์ผ์˜ ์ฒซ 8๋ฐ”์ดํŠธ๋งŒ ์ฝ๊ธฐ
            header = f.read(8)
        
        # ์˜ˆ์ƒ๋˜๋Š” ํ™•์žฅ์ž์˜ ๋งค์ง ๋ฐ”์ดํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
        expected_bytes = MAGIC_BYTES.get(expected_extension.lower())
        
        if not expected_bytes:
            return False
        
        # ๋งค์ง ๋ฐ”์ดํŠธ ๋น„๊ต
        return header.startswith(expected_bytes)
    
    except Exception as e:
        print(f"ํŒŒ์ผ ๊ฒ€์ฆ ์˜ค๋ฅ˜: {e}")
        return False

@app.route('/upload', methods=['POST'])
def upload_file():
    # ... ์ด์ „ ์ฝ”๋“œ ...
    
    # ํŒŒ์ผ ์ €์žฅ
    file.save(filepath)
    
    # ๋งค์ง ๋ฐ”์ดํŠธ ๊ฒ€์ฆ
    ext = safe_filename.rsplit('.', 1)[1].lower()
    if not verify_file_type(filepath, ext):
        # ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ํŒŒ์ผ ์‚ญ์ œ
        os.remove(filepath)
        return jsonify({
            'error': 'ํŒŒ์ผ ํ˜•์‹์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์œ„์กฐ๋œ ํŒŒ์ผ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.'
        }), 400
    
    return jsonify({
        'message': 'ํŒŒ์ผ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
        'filename': safe_filename
    }), 200

๐Ÿ–ผ๏ธ 3๋‹จ๊ณ„: ์ด๋ฏธ์ง€ ํŒŒ์ผ ์žฌ์ฒ˜๋ฆฌ

์ด๋ฏธ์ง€ ํŒŒ์ผ์˜ ๊ฒฝ์šฐ, ํ•œ ๋ฒˆ ๋” ์•ˆ์ „์žฅ์น˜๋ฅผ ๋‘˜ ์ˆ˜ ์žˆ์–ด. ๋ฐ”๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋‹ค์‹œ ๊ทธ๋ฆฌ๋Š” ๊ฑฐ์•ผ! ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ด๋ฏธ์ง€ ์•ˆ์— ์ˆจ๊ฒจ์ง„ ์•…์„ฑ ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ . ๐ŸŽจ
๐ŸŽจ ์ด๋ฏธ์ง€ ์žฌ์ฒ˜๋ฆฌ (Node.js with Sharp)
const sharp = require('sharp');

async function sanitizeImage(inputPath, outputPath) {
  try {
    // ์ด๋ฏธ์ง€๋ฅผ ์ฝ๊ณ  ๋‹ค์‹œ ์ธ์ฝ”๋”ฉ
    // ์ด ๊ณผ์ •์—์„œ ์ˆจ๊ฒจ์ง„ ์•…์„ฑ ์ฝ”๋“œ๊ฐ€ ์ œ๊ฑฐ๋จ
    await sharp(inputPath)
      .resize(2000, 2000, { // ์ตœ๋Œ€ ํฌ๊ธฐ ์ œํ•œ
        fit: 'inside',
        withoutEnlargement: true
      })
      .jpeg({ quality: 90 }) // ๋˜๋Š” .png()
      .toFile(outputPath);
    
    return true;
  } catch (error) {
    console.error('์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜:', error);
    return false;
  }
}

app.post('/upload-image', upload.single('image'), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: '์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.' });
  }
  
  const tempPath = req.file.path;
  const safePath = tempPath + '_safe.jpg';
  
  // ์ด๋ฏธ์ง€ ์žฌ์ฒ˜๋ฆฌ
  const success = await sanitizeImage(tempPath, safePath);
  
  if (!success) {
    await fs.unlink(tempPath);
    return res.status(400).json({ 
      error: '์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์†์ƒ๋œ ํŒŒ์ผ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.' 
    });
  }
  
  // ์›๋ณธ ํŒŒ์ผ ์‚ญ์ œ, ์•ˆ์ „ํ•œ ํŒŒ์ผ๋งŒ ์œ ์ง€
  await fs.unlink(tempPath);
  await fs.rename(safePath, tempPath);
  
  res.json({ 
    message: '์ด๋ฏธ์ง€๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
    filename: req.file.filename 
  });
});
๐ŸŽจ ์ด๋ฏธ์ง€ ์žฌ์ฒ˜๋ฆฌ (Python with Pillow)
from PIL import Image
import os

def sanitize_image(input_path, output_path, max_size=(2000, 2000)):
    """์ด๋ฏธ์ง€๋ฅผ ์žฌ์ฒ˜๋ฆฌํ•˜์—ฌ ์•…์„ฑ ์ฝ”๋“œ ์ œ๊ฑฐ"""
    try:
        # ์ด๋ฏธ์ง€ ์—ด๊ธฐ
        with Image.open(input_path) as img:
            # RGB ๋ชจ๋“œ๋กœ ๋ณ€ํ™˜ (ํˆฌ๋ช…๋„ ์ œ๊ฑฐ)
            if img.mode in ('RGBA', 'LA', 'P'):
                background = Image.new('RGB', img.size, (255, 255, 255))
                if img.mode == 'P':
                    img = img.convert('RGBA')
                background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
                img = background
            
            # ํฌ๊ธฐ ์กฐ์ •
            img.thumbnail(max_size, Image.Resampling.LANCZOS)
            
            # ์ƒˆ ํŒŒ์ผ๋กœ ์ €์žฅ (EXIF ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ๋จ)
            img.save(output_path, 'JPEG', quality=90, optimize=True)
        
        return True
    except Exception as e:
        print(f"์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {e}")
        return False

@app.route('/upload-image', methods=['POST'])
def upload_image():
    # ... ์ด์ „ ๊ฒ€์ฆ ์ฝ”๋“œ ...
    
    # ํŒŒ์ผ ์ €์žฅ
    temp_path = filepath
    safe_path = filepath + '_safe.jpg'
    
    # ์ด๋ฏธ์ง€ ์žฌ์ฒ˜๋ฆฌ
    if not sanitize_image(temp_path, safe_path):
        os.remove(temp_path)
        return jsonify({
            'error': '์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์†์ƒ๋œ ํŒŒ์ผ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.'
        }), 400
    
    # ์›๋ณธ ์‚ญ์ œ, ์•ˆ์ „ํ•œ ํŒŒ์ผ๋กœ ๊ต์ฒด
    os.remove(temp_path)
    os.rename(safe_path, temp_path)
    
    return jsonify({
        'message': '์ด๋ฏธ์ง€๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
        'filename': safe_filename
    }), 200

๐Ÿ—‚๏ธ 4๋‹จ๊ณ„: ์•ˆ์ „ํ•œ ํŒŒ์ผ ์ €์žฅ ์œ„์น˜

โš ๏ธ ์ ˆ๋Œ€ ํ•˜๋ฉด ์•ˆ ๋˜๋Š” ๊ฒƒ ์—…๋กœ๋“œ๋œ ํŒŒ์ผ์„ ์›น ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ(public, static ํด๋” ๋“ฑ)์— ์ง์ ‘ ์ €์žฅํ•˜๋ฉด ์•ˆ ๋ผ! ํ•ด์ปค๊ฐ€ ์—…๋กœ๋“œํ•œ ์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ์„ ๋ธŒ๋ผ์šฐ์ €๋กœ ์ง์ ‘ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ .
โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• 1. ์›น ๋ฃจํŠธ ๋ฐ–์˜ ๋ณ„๋„ ๋””๋ ‰ํ† ๋ฆฌ์— ์ €์žฅ
2. ํŒŒ์ผ ์ ‘๊ทผ์€ ๋ณ„๋„์˜ ๋‹ค์šด๋กœ๋“œ API๋ฅผ ํ†ตํ•ด์„œ๋งŒ ๊ฐ€๋Šฅํ•˜๊ฒŒ
3. ๋‹ค์šด๋กœ๋“œ ์‹œ ์ ์ ˆํ•œ Content-Type ํ—ค๋” ์„ค์ •
4. Content-Disposition ํ—ค๋”๋กœ ๋‹ค์šด๋กœ๋“œ ๊ฐ•์ œ
๐Ÿ“ ์•ˆ์ „ํ•œ ํŒŒ์ผ ์„œ๋น™ (Node.js)
const express = require('express');
const path = require('path');
const fs = require('fs').promises;

// ์—…๋กœ๋“œ ๋””๋ ‰ํ† ๋ฆฌ (์›น ๋ฃจํŠธ ๋ฐ–!)
const UPLOAD_DIR = '/var/app/secure_uploads/';

// ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ API
app.get('/download/:filename', async (req, res) => {
  try {
    const filename = req.params.filename;
    
    // ๊ฒฝ๋กœ ์กฐ์ž‘ ๊ณต๊ฒฉ ๋ฐฉ์ง€
    if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
      return res.status(400).json({ error: '์ž˜๋ชป๋œ ํŒŒ์ผ๋ช…์ž…๋‹ˆ๋‹ค.' });
    }
    
    const filepath = path.join(UPLOAD_DIR, filename);
    
    // ํŒŒ์ผ ์กด์žฌ ํ™•์ธ
    try {
      await fs.access(filepath);
    } catch {
      return res.status(404).json({ error: 'ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' });
    }
    
    // ํŒŒ์ผ ํ™•์žฅ์ž์— ๋”ฐ๋ฅธ MIME ํƒ€์ž… ์„ค์ •
    const ext = path.extname(filename).toLowerCase();
    const mimeTypes = {
      '.jpg': 'image/jpeg',
      '.jpeg': 'image/jpeg',
      '.png': 'image/png',
      '.gif': 'image/gif',
      '.pdf': 'application/pdf',
    };
    
    const mimeType = mimeTypes[ext] || 'application/octet-stream';
    
    // ๋ณด์•ˆ ํ—ค๋” ์„ค์ •
    res.setHeader('Content-Type', mimeType);
    res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
    res.setHeader('X-Content-Type-Options', 'nosniff'); // MIME ํƒ€์ž… ์Šค๋‹ˆํ•‘ ๋ฐฉ์ง€
    res.setHeader('Content-Security-Policy', "default-src 'none'"); // ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ๋ฐฉ์ง€
    
    // ํŒŒ์ผ ์ „์†ก
    const fileStream = require('fs').createReadStream(filepath);
    fileStream.pipe(res);
    
  } catch (error) {
    console.error('ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์˜ค๋ฅ˜:', error);
    res.status(500).json({ error: 'ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.' });
  }
});

// ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ API (๋” ์—„๊ฒฉํ•œ ์ œํ•œ)
app.get('/preview/:filename', async (req, res) => {
  try {
    const filename = req.params.filename;
    
    // ๊ฒฝ๋กœ ์กฐ์ž‘ ๋ฐฉ์ง€
    if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
      return res.status(400).json({ error: '์ž˜๋ชป๋œ ํŒŒ์ผ๋ช…์ž…๋‹ˆ๋‹ค.' });
    }
    
    // ์ด๋ฏธ์ง€ ํŒŒ์ผ๋งŒ ํ—ˆ์šฉ
    const ext = path.extname(filename).toLowerCase();
    if (!['.jpg', '.jpeg', '.png', '.gif'].includes(ext)) {
      return res.status(400).json({ error: '์ด๋ฏธ์ง€ ํŒŒ์ผ๋งŒ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.' });
    }
    
    const filepath = path.join(UPLOAD_DIR, filename);
    
    // ํŒŒ์ผ ์กด์žฌ ํ™•์ธ
    try {
      await fs.access(filepath);
    } catch {
      return res.status(404).json({ error: 'ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' });
    }
    
    // ์ด๋ฏธ์ง€ MIME ํƒ€์ž… ์„ค์ •
    const mimeTypes = {
      '.jpg': 'image/jpeg',
      '.jpeg': 'image/jpeg',
      '.png': 'image/png',
      '.gif': 'image/gif',
    };
    
    // ๋ณด์•ˆ ํ—ค๋” ์„ค์ •
    res.setHeader('Content-Type', mimeTypes[ext]);
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'");
    
    // ํŒŒ์ผ ์ „์†ก
    const fileStream = require('fs').createReadStream(filepath);
    fileStream.pipe(res);
    
  } catch (error) {
    console.error('์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์˜ค๋ฅ˜:', error);
    res.status(500).json({ error: '์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.' });
  }
});

๐Ÿ” 5๋‹จ๊ณ„: ์ถ”๊ฐ€ ๋ณด์•ˆ ์กฐ์น˜๋“ค

์—ฌ๊ธฐ๊นŒ์ง€ ํ–ˆ์œผ๋ฉด ๊ธฐ๋ณธ์€ ๋‹ค ํ•œ ๊ฑฐ์•ผ! ํ•˜์ง€๋งŒ ๋” ๊ฐ•๋ ฅํ•œ ๋ณด์•ˆ์„ ์›ํ•œ๋‹ค๋ฉด ์ด๋Ÿฐ ๊ฒƒ๋“ค๋„ ๊ณ ๋ คํ•ด๋ด:
1 ๋ฐ”์ด๋Ÿฌ์Šค ์Šค์บ” ํ†ตํ•ฉ

ClamAV ๊ฐ™์€ ์•ˆํ‹ฐ๋ฐ”์ด๋Ÿฌ์Šค ์—”์ง„์„ ํ†ตํ•ฉํ•ด์„œ ์—…๋กœ๋“œ๋œ ํŒŒ์ผ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์Šค์บ”ํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์€ ์„œ๋น„์Šค๋ผ๋ฉด ํ•„์ˆ˜!
๐Ÿฆ  ClamAV ํ†ตํ•ฉ ์˜ˆ์‹œ (Node.js)
const NodeClam = require('clamscan');

// ClamAV ์ดˆ๊ธฐํ™”
const clamscan = await new NodeClam().init({
  clamdscan: {
    host: 'localhost',
    port: 3310,
  }
});

async function scanFile(filePath) {
  try {
    const { isInfected, viruses } = await clamscan.isInfected(filePath);
    
    if (isInfected) {
      console.log(`์•…์„ฑ ํŒŒ์ผ ํƒ์ง€: ${viruses.join(', ')}`);
      return false;
    }
    
    return true;
  } catch (error) {
    console.error('๋ฐ”์ด๋Ÿฌ์Šค ์Šค์บ” ์˜ค๋ฅ˜:', error);
    return false; // ์Šค์บ” ์‹คํŒจ ์‹œ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฑฐ๋ถ€
  }
}

app.post('/upload', upload.single('file'), async (req, res) => {
  // ... ์ด์ „ ๊ฒ€์ฆ ์ฝ”๋“œ ...
  
  // ๋ฐ”์ด๋Ÿฌ์Šค ์Šค์บ”
  const isSafe = await scanFile(req.file.path);
  
  if (!isSafe) {
    await fs.unlink(req.file.path);
    return res.status(400).json({ 
      error: '์•…์„ฑ ํŒŒ์ผ์ด ํƒ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.' 
    });
  }
  
  res.json({ message: 'ํŒŒ์ผ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.' });
});
2 ์—…๋กœ๋“œ ์†๋„ ์ œํ•œ (Rate Limiting)

๊ฐ™์€ IP์—์„œ ์งง์€ ์‹œ๊ฐ„์— ๋„ˆ๋ฌด ๋งŽ์€ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜์ง€ ๋ชปํ•˜๋„๋ก ์ œํ•œํ•ด์•ผ ํ•ด. DoS ๊ณต๊ฒฉ์„ ๋ง‰์„ ์ˆ˜ ์žˆ๊ฑฐ๋“ !
โฑ๏ธ Rate Limiting ๊ตฌํ˜„
const rateLimit = require('express-rate-limit');

// ์—…๋กœ๋“œ ์†๋„ ์ œํ•œ
const uploadLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15๋ถ„
  max: 10, // 15๋ถ„๋‹น ์ตœ๋Œ€ 10๊ฐœ ํŒŒ์ผ
  message: '๋„ˆ๋ฌด ๋งŽ์€ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
  standardHeaders: true,
  legacyHeaders: false,
});

// ๋ผ์šฐํŠธ์— ์ ์šฉ
app.post('/upload', uploadLimiter, upload.single('file'), async (req, res) => {
  // ... ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ ...
});
3 ํŒŒ์ผ๋ช… ์•”ํ˜ธํ™” ๋ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งคํ•‘

์‹ค์ œ ํŒŒ์ผ๋ช…์„ ์™„์ „ํžˆ ์ˆจ๊ธฐ๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋งคํ•‘ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” ๊ฑฐ์•ผ. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ•ด์ปค๊ฐ€ ํŒŒ์ผ ๊ตฌ์กฐ๋ฅผ ์ถ”์ธกํ•˜๊ธฐ ์–ด๋ ค์›Œ์ ธ.
๐Ÿ”’ ํŒŒ์ผ๋ช… ์•”ํ˜ธํ™” ์‹œ์Šคํ…œ
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');

// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ (์˜ˆ์‹œ)
// files ํ…Œ์ด๋ธ”:
// - id: UUID (Primary Key)
// - original_name: ์›๋ณธ ํŒŒ์ผ๋ช… (์•”ํ˜ธํ™”)
// - stored_name: ์‹ค์ œ ์ €์žฅ๋œ ํŒŒ์ผ๋ช… (UUID)
// - user_id: ์—…๋กœ๋“œํ•œ ์‚ฌ์šฉ์ž
// - upload_date: ์—…๋กœ๋“œ ๋‚ ์งœ
// - file_size: ํŒŒ์ผ ํฌ๊ธฐ
// - mime_type: MIME ํƒ€์ž…

function encryptFilename(filename, secretKey) {
  const cipher = crypto.createCipher('aes-256-cbc', secretKey);
  let encrypted = cipher.update(filename, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return encrypted;
}

function decryptFilename(encryptedFilename, secretKey) {
  const decipher = crypto.createDecipher('aes-256-cbc', secretKey);
  let decrypted = decipher.update(encryptedFilename, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

app.post('/upload', upload.single('file'), async (req, res) => {
  // ... ๋ณด์•ˆ ๊ฒ€์ฆ ...
  
  const fileId = uuidv4();
  const storedName = `${fileId}${path.extname(req.file.originalname)}`;
  const encryptedOriginalName = encryptFilename(
    req.file.originalname, 
    process.env.ENCRYPTION_KEY
  );
  
  // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ
  await db.query(`
    INSERT INTO files (id, original_name, stored_name, user_id, file_size, mime_type)
    VALUES (?, ?, ?, ?, ?, ?)
  `, [
    fileId,
    encryptedOriginalName,
    storedName,
    req.user.id,
    req.file.size,
    req.file.mimetype
  ]);
  
  // ํŒŒ์ผ์„ UUID ์ด๋ฆ„์œผ๋กœ ์ €์žฅ
  await fs.rename(req.file.path, path.join(UPLOAD_DIR, storedName));
  
  res.json({ 
    message: 'ํŒŒ์ผ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
    fileId: fileId // ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ๋Š” UUID๋งŒ ์ „๋‹ฌ
  });
});

// ๋‹ค์šด๋กœ๋“œ ์‹œ UUID๋กœ ์กฐํšŒ
app.get('/download/:fileId', async (req, res) => {
  const fileId = req.params.fileId;
  
  // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ
  const file = await db.query('SELECT * FROM files WHERE id = ?', [fileId]);
  
  if (!file) {
    return res.status(404).json({ error: 'ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' });
  }
  
  // ์›๋ณธ ํŒŒ์ผ๋ช… ๋ณตํ˜ธํ™”
  const originalName = decryptFilename(file.original_name, process.env.ENCRYPTION_KEY);
  
  // ํŒŒ์ผ ์ „์†ก
  res.setHeader('Content-Disposition', `attachment; filename="${originalName}"`);
  // ... ๋‚˜๋จธ์ง€ ํŒŒ์ผ ์ „์†ก ๋กœ์ง ...
});
4 CDN ๋ฐ ์˜ค๋ธŒ์ ํŠธ ์Šคํ† ๋ฆฌ์ง€ ํ™œ์šฉ

AWS S3, Google Cloud Storage ๊ฐ™์€ ์˜ค๋ธŒ์ ํŠธ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ณด์•ˆ์ด ๋” ๊ฐ•ํ™”๋ผ. ์ด๋Ÿฐ ์„œ๋น„์Šค๋“ค์€ ์ž์ฒด์ ์œผ๋กœ ๊ฐ•๋ ฅํ•œ ๋ณด์•ˆ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๊ฑฐ๋“ !
โ˜๏ธ AWS S3 ์•ˆ์ „ํ•œ ์—…๋กœ๋“œ
const AWS = require('aws-sdk');
const s3 = new AWS.S3({
  accessKeyId: process.env.AWS_ACCESS_KEY,
  secretAccessKey: process.env.AWS_SECRET_KEY,
  region: process.env.AWS_REGION
});

async function uploadToS3(file, filename) {
  const params = {
    Bucket: process.env.S3_BUCKET,
    Key: `uploads/${filename}`,
    Body: file.buffer,
    ContentType: file.mimetype,
    // ์ค‘์š”: ํŒŒ์ผ์„ private์œผ๋กœ ์„ค์ •
    ACL: 'private',
    // ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
    Metadata: {
      'original-name': file.originalname,
      'upload-date': new Date().toISOString()
    },
    // ์„œ๋ฒ„ ์ธก ์•”ํ˜ธํ™”
    ServerSideEncryption: 'AES256'
  };
  
  return s3.upload(params).promise();
}

// Presigned URL๋กœ ์•ˆ์ „ํ•œ ๋‹ค์šด๋กœ๋“œ ์ œ๊ณต
async function getSecureDownloadUrl(filename, expiresIn = 3600) {
  const params = {
    Bucket: process.env.S3_BUCKET,
    Key: `uploads/${filename}`,
    Expires: expiresIn, // 1์‹œ๊ฐ„ ํ›„ ๋งŒ๋ฃŒ
    ResponseContentDisposition: `attachment; filename="${filename}"`
  };
  
  return s3.getSignedUrl('getObject', params);
}

app.get('/download/:fileId', async (req, res) => {
  // ... ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ ...
  
  // ์ž„์‹œ ๋‹ค์šด๋กœ๋“œ URL ์ƒ์„ฑ (1์‹œ๊ฐ„ ์œ ํšจ)
  const downloadUrl = await getSecureDownloadUrl(file.stored_name);
  
  res.json({ downloadUrl });
});
๋ณด์•ˆ ๊ณ„์ธต ๊ตฌ์กฐ 1. ํ™•์žฅ์ž ๊ฒ€์ฆ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ 2. ๋งค์ง๋ฐ”์ดํŠธ ์‹ค์ œ ํŒŒ์ผ ๊ฒ€์ฆ 3. ํŒŒ์ผ ์žฌ์ฒ˜๋ฆฌ ์•…์„ฑ์ฝ”๋“œ ์ œ๊ฑฐ 4. ์•ˆ์ „ํ•œ ์ €์žฅ ๊ฒฉ๋ฆฌ๋œ ๋””๋ ‰ํ† ๋ฆฌ 5. ๋ฐ”์ด๋Ÿฌ์Šค ์Šค์บ” ์‹ค์‹œ๊ฐ„ ๊ฒ€์‚ฌ 6. Rate Limiting DoS ๋ฐฉ์–ด 7. ์•”ํ˜ธํ™” ํŒŒ์ผ๋ช… ๋ณดํ˜ธ ๐Ÿ›ก๏ธ

๐ŸŽ“ ์‹ค์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ: ๋‚ด ์ฝ”๋“œ๋Š” ์•ˆ์ „ํ• ๊นŒ?

์ž, ์ด์ œ ์‹ค์ „์ด์•ผ! ๋‚ด๊ฐ€ ๋งŒ๋“  ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์ด ์•ˆ์ „ํ•œ์ง€ ์ฒดํฌํ•ด๋ณด์ž. ์ด ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ํ•˜๋‚˜์”ฉ ํ™•์ธํ•ด๋ด! โœ…
ํ•ญ๋ชฉ ์ฒดํฌ ํฌ์ธํŠธ ์œ„ํ—˜๋„
โœ… ํ™•์žฅ์ž ๊ฒ€์ฆ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๋ฐฉ์‹์œผ๋กœ ํ—ˆ์šฉ๋œ ํ™•์žฅ์ž๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•œ๊ฐ€? ๐Ÿ”ด Critical
โœ… ์ด์ค‘ ํ™•์žฅ์ž file.php.jpg ๊ฐ™์€ ์ด์ค‘ ํ™•์žฅ์ž๋ฅผ ์ฐจ๋‹จํ•˜๋Š”๊ฐ€? ๐Ÿ”ด Critical
โœ… ๋งค์ง ๋ฐ”์ดํŠธ ํŒŒ์ผ์˜ ์‹ค์ œ ๋‚ด์šฉ์„ ๊ฒ€์ฆํ•˜๋Š”๊ฐ€? ๐Ÿ”ด Critical
โœ… ํŒŒ์ผ ํฌ๊ธฐ ์ตœ๋Œ€ ํŒŒ์ผ ํฌ๊ธฐ ์ œํ•œ์ด ์žˆ๋Š”๊ฐ€? ๐ŸŸก High
โœ… ํŒŒ์ผ๋ช… ๊ฒ€์ฆ ๊ฒฝ๋กœ ์กฐ์ž‘ ๋ฌธ์ž(../, ..\)๋ฅผ ์ฐจ๋‹จํ•˜๋Š”๊ฐ€? ๐Ÿ”ด Critical
โœ… ์ €์žฅ ์œ„์น˜ ์›น ๋ฃจํŠธ ๋ฐ–์˜ ์•ˆ์ „ํ•œ ๋””๋ ‰ํ† ๋ฆฌ์— ์ €์žฅํ•˜๋Š”๊ฐ€? ๐Ÿ”ด Critical
โœ… ํŒŒ์ผ๋ช… ๋ณ€๊ฒฝ ์›๋ณธ ํŒŒ์ผ๋ช…์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๊ฐ€? ๐ŸŸก High
โœ… ์‹คํ–‰ ๊ถŒํ•œ ์—…๋กœ๋“œ ๋””๋ ‰ํ† ๋ฆฌ์— ์‹คํ–‰ ๊ถŒํ•œ์ด ์—†๋Š”๊ฐ€? ๐Ÿ”ด Critical
โœ… Content-Type ๋‹ค์šด๋กœ๋“œ ์‹œ ์˜ฌ๋ฐ”๋ฅธ Content-Type์„ ์„ค์ •ํ•˜๋Š”๊ฐ€? ๐ŸŸก High
โœ… Rate Limiting ์—…๋กœ๋“œ ์†๋„ ์ œํ•œ์ด ์žˆ๋Š”๊ฐ€? ๐ŸŸก High
โœ… ๋ฐ”์ด๋Ÿฌ์Šค ์Šค์บ” ์—…๋กœ๋“œ๋œ ํŒŒ์ผ์„ ์Šค์บ”ํ•˜๋Š”๊ฐ€? ๐ŸŸข Medium
โœ… ๋กœ๊น… ์—…๋กœ๋“œ ํ™œ๋™์„ ๋กœ๊ทธ๋กœ ๊ธฐ๋กํ•˜๋Š”๊ฐ€? ๐ŸŸข Medium
๐Ÿšจ ๊ธด๊ธ‰ ์ ๊ฒ€ ํ•„์š”! ์œ„ ํ•ญ๋ชฉ ์ค‘ ๐Ÿ”ด Critical ํ‘œ์‹œ๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์ฒดํฌ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด, ์ง€๊ธˆ ๋‹น์žฅ ์ˆ˜์ •ํ•ด์•ผ ํ•ด! ์‹ฌ๊ฐํ•œ ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ์กด์žฌํ•˜๋Š” ๊ฑฐ์•ผ.

๐Ÿ”ฌ ์‹ค์ œ ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค์™€ ๋ฐฉ์–ด ์ „๋žต

์ด๋ก ๋งŒ ์•Œ๋ฉด ๋ญํ•ด? ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๊ณต๊ฒฉ๋‹นํ•˜๊ณ  ์–ด๋–ป๊ฒŒ ๋ง‰๋Š”์ง€ ์•Œ์•„์•ผ์ง€! ์‹ค์ „ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์‚ดํŽด๋ณด์ž. ๐ŸŽฌ

์‹œ๋‚˜๋ฆฌ์˜ค 1: PHP ์›น์…ธ ์—…๋กœ๋“œ ๊ณต๊ฒฉ

๐ŸŽฏ ๊ณต๊ฒฉ์ž์˜ ์ „๋žต

1. ํ”„๋กœํ•„ ์‚ฌ์ง„ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ๋ฐœ๊ฒฌ
2. ์•…์„ฑ PHP ์ฝ”๋“œ๋ฅผ ์ด๋ฏธ์ง€ ํŒŒ์ผ๋กœ ์œ„์žฅ
3. ํŒŒ์ผ๋ช…์„ avatar.php๋กœ ์—…๋กœ๋“œ ์‹œ๋„
4. ์ฐจ๋‹จ๋˜๋ฉด avatar.php.jpg๋กœ ์žฌ์‹œ๋„
5. ์—…๋กœ๋“œ ์„ฑ๊ณต ์‹œ ๋ธŒ๋ผ์šฐ์ €๋กœ ์ง์ ‘ ์ ‘๊ทผํ•˜์—ฌ ์‹คํ–‰
๐Ÿ›ก๏ธ ๋ฐฉ์–ด ์ „๋žต

1๋‹จ๊ณ„: ํ™•์žฅ์ž ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋กœ .php ์ฐจ๋‹จ
2๋‹จ๊ณ„: ์ด์ค‘ ํ™•์žฅ์ž ๊ฒ€์‚ฌ๋กœ .php.jpg ์ฐจ๋‹จ
3๋‹จ๊ณ„: ๋งค์ง ๋ฐ”์ดํŠธ ๊ฒ€์ฆ์œผ๋กœ ์‹ค์ œ PHP ์ฝ”๋“œ ํƒ์ง€
4๋‹จ๊ณ„: ์ด๋ฏธ์ง€ ์žฌ์ฒ˜๋ฆฌ๋กœ ์ˆจ๊ฒจ์ง„ ์ฝ”๋“œ ์ œ๊ฑฐ
5๋‹จ๊ณ„: ์›น ๋ฃจํŠธ ๋ฐ–์— ์ €์žฅํ•˜์—ฌ ์ง์ ‘ ์‹คํ–‰ ๋ถˆ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ฆ
๐Ÿ›ก๏ธ ์™„๋ฒฝํ•œ ๋ฐฉ์–ด ์ฝ”๋“œ
async function secureImageUpload(file) {
  // 1. ํ™•์žฅ์ž ๊ฒ€์ฆ
  const ext = path.extname(file.originalname).toLowerCase();
  const allowedExts = ['.jpg', '.jpeg', '.png', '.gif'];
  
  if (!allowedExts.includes(ext)) {
    throw new Error('ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค.');
  }
  
  // 2. ์ด์ค‘ ํ™•์žฅ์ž ๊ฒ€์‚ฌ
  const nameWithoutExt = path.basename(file.originalname, ext);
  if (nameWithoutExt.includes('.')) {
    throw new Error('์ด์ค‘ ํ™•์žฅ์ž๋Š” ํ—ˆ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
  }
  
  // 3. ๋งค์ง ๋ฐ”์ดํŠธ ๊ฒ€์ฆ
  const isValidImage = await verifyFileType(file.path, ext.slice(1));
  if (!isValidImage) {
    await fs.unlink(file.path);
    throw new Error('์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฏธ์ง€ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค.');
  }
  
  // 4. ์ด๋ฏธ์ง€ ์žฌ์ฒ˜๋ฆฌ
  const safePath = file.path + '_safe' + ext;
  const processed = await sanitizeImage(file.path, safePath);
  if (!processed) {
    await fs.unlink(file.path);
    throw new Error('์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
  }
  
  // 5. ์•ˆ์ „ํ•œ ํŒŒ์ผ๋ช… ์ƒ์„ฑ
  const safeFilename = `${uuidv4()}${ext}`;
  const finalPath = path.join(SECURE_UPLOAD_DIR, safeFilename);
  
  // 6. ์›น ๋ฃจํŠธ ๋ฐ–์œผ๋กœ ์ด๋™
  await fs.rename(safePath, finalPath);
  await fs.unlink(file.path);
  
  return safeFilename;
}

์‹œ๋‚˜๋ฆฌ์˜ค 2: ๊ฒฝ๋กœ ์กฐ์ž‘(Path Traversal) ๊ณต๊ฒฉ

๐ŸŽฏ ๊ณต๊ฒฉ์ž์˜ ์ „๋žต

ํŒŒ์ผ๋ช…์— ../๋ฅผ ํฌํ•จ์‹œ์ผœ์„œ ์‹œ์Šคํ…œ์˜ ์ค‘์š”ํ•œ ํŒŒ์ผ์„ ๋ฎ์–ด์“ฐ๋ ค๊ณ  ์‹œ๋„ํ•ด. ์˜ˆ๋ฅผ ๋“ค์–ด:
โ€ข ../../../../etc/passwd
โ€ข ..\..\..\..\windows\system32\config\sam
โ€ข ../../.ssh/authorized_keys
๐Ÿ›ก๏ธ ๋ฐฉ์–ด ์ „๋žต
๐Ÿ›ก๏ธ ๊ฒฝ๋กœ ์กฐ์ž‘ ๋ฐฉ์–ด ์ฝ”๋“œ
function sanitizeFilename(filename) {
  // 1. ๊ฒฝ๋กœ ๊ตฌ๋ถ„์ž ์ œ๊ฑฐ
  let safe = filename.replace(/[\/\\]/g, '');
  
  // 2. ์ƒ๋Œ€ ๊ฒฝ๋กœ ๋ฌธ์ž ์ œ๊ฑฐ
  safe = safe.replace(/\.\./g, '');
  
  // 3. ํŠน์ˆ˜ ๋ฌธ์ž ์ œ๊ฑฐ
  safe = safe.replace(/[^a-zA-Z0-9._-]/g, '');
  
  // 4. ์ตœ๋Œ€ ๊ธธ์ด ์ œํ•œ
  if (safe.length > 255) {
    safe = safe.substring(0, 255);
  }
  
  // 5. ๋นˆ ๋ฌธ์ž์—ด ์ฒดํฌ
  if (safe.length === 0) {
    throw new Error('์œ ํšจํ•˜์ง€ ์•Š์€ ํŒŒ์ผ๋ช…์ž…๋‹ˆ๋‹ค.');
  }
  
  return safe;
}

// ๋” ์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ•: ์›๋ณธ ํŒŒ์ผ๋ช…์„ ์•„์˜ˆ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ
function generateSecureFilename(originalFilename) {
  const ext = path.extname(originalFilename).toLowerCase();
  
  // UUID + ํƒ€์ž„์Šคํƒฌํ”„๋กœ ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ์ด๋ฆ„ ์ƒ์„ฑ
  const timestamp = Date.now();
  const random = crypto.randomBytes(16).toString('hex');
  
  return `${timestamp}-${random}${ext}`;
}

์‹œ๋‚˜๋ฆฌ์˜ค 3: ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ DoS ๊ณต๊ฒฉ

๐ŸŽฏ ๊ณต๊ฒฉ์ž์˜ ์ „๋žต

์—ฌ๋Ÿฌ ๊ณ„์ •์œผ๋กœ ๋™์‹œ์— ์ˆ˜๋ฐฑ ๊ฐœ์˜ ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ(๊ฐ 100MB ์ด์ƒ)์„ ์—…๋กœ๋“œํ•ด์„œ ์„œ๋ฒ„์˜ ๋””์Šคํฌ ๊ณต๊ฐ„๊ณผ ๋Œ€์—ญํญ์„ ๊ณ ๊ฐˆ์‹œํ‚ค๋ ค๊ณ  ํ•ด.
๐Ÿ›ก๏ธ ๋ฐฉ์–ด ์ „๋žต
๐Ÿ›ก๏ธ ๋‹ค์ธต ๋ฐฉ์–ด ์‹œ์Šคํ…œ
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');

// 1. ํŒŒ์ผ ํฌ๊ธฐ ์ œํ•œ
const upload = multer({
  storage: storage,
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB
    files: 1 // ํ•œ ๋ฒˆ์— 1๊ฐœ๋งŒ
  }
});

// 2. IP๋ณ„ ์—…๋กœ๋“œ ํšŸ์ˆ˜ ์ œํ•œ
const uploadLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15๋ถ„
  max: 10, // 15๋ถ„๋‹น ์ตœ๋Œ€ 10๊ฐœ
  message: '์—…๋กœ๋“œ ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค.',
  standardHeaders: true,
  legacyHeaders: false,
  // IP๋ณ„๋กœ ๋‹ค๋ฅธ ์ œํ•œ ์ ์šฉ ๊ฐ€๋Šฅ
  skip: (req) => {
    // ์ธ์ฆ๋œ ํ”„๋ฆฌ๋ฏธ์—„ ์‚ฌ์šฉ์ž๋Š” ์ œํ•œ ์™„ํ™”
    return req.user && req.user.isPremium;
  }
});

// 3. ์†๋„ ์ œํ•œ (์ ์ง„์  ์ง€์—ฐ)
const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 5, // 5๊ฐœ ์—…๋กœ๋“œ ํ›„๋ถ€ํ„ฐ ์ง€์—ฐ ์‹œ์ž‘
  delayMs: 500 // ๋งค ์š”์ฒญ๋งˆ๋‹ค 500ms์”ฉ ์ง€์—ฐ ์ฆ๊ฐ€
});

// 4. ์‚ฌ์šฉ์ž๋ณ„ ์ด ์ €์žฅ ๊ณต๊ฐ„ ์ œํ•œ
async function checkUserQuota(userId, fileSize) {
  const userFiles = await db.query(
    'SELECT SUM(file_size) as total FROM files WHERE user_id = ?',
    [userId]
  );
  
  const currentTotal = userFiles[0].total || 0;
  const maxQuota = 100 * 1024 * 1024; // 100MB
  
  if (currentTotal + fileSize > maxQuota) {
    throw new Error('์ €์žฅ ๊ณต๊ฐ„์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.');
  }
}

// 5. ์ „์ฒด ์‹œ์Šคํ…œ ๋””์Šคํฌ ๊ณต๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง
const diskspace = require('diskspace');

async function checkSystemDiskSpace() {
  return new Promise((resolve, reject) => {
    diskspace.check('/', (err, result) => {
      if (err) return reject(err);
      
      const freeSpaceGB = result.free / (1024 * 1024 * 1024);
      
      // ์—ฌ์œ  ๊ณต๊ฐ„์ด 10GB ๋ฏธ๋งŒ์ด๋ฉด ์—…๋กœ๋“œ ์ฐจ๋‹จ
      if (freeSpaceGB < 10) {
        reject(new Error('์„œ๋ฒ„ ์ €์žฅ ๊ณต๊ฐ„์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.'));
      }
      
      resolve(true);
    });
  });
}

// ๋ชจ๋“  ๋ฐฉ์–ด ๊ณ„์ธต ์ ์šฉ
app.post('/upload',
  uploadLimiter,
  speedLimiter,
  upload.single('file'),
  async (req, res) => {
    try {
      // ์‹œ์Šคํ…œ ๋””์Šคํฌ ๊ณต๊ฐ„ ์ฒดํฌ
      await checkSystemDiskSpace();
      
      // ์‚ฌ์šฉ์ž ํ• ๋‹น๋Ÿ‰ ์ฒดํฌ
      await checkUserQuota(req.user.id, req.file.size);
      
      // ํŒŒ์ผ ์ฒ˜๋ฆฌ
      const safeFilename = await secureImageUpload(req.file);
      
      // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๊ธฐ๋ก
      await db.query(
        'INSERT INTO files (user_id, filename, file_size) VALUES (?, ?, ?)',
        [req.user.id, safeFilename, req.file.size]
      );
      
      res.json({ 
        message: 'ํŒŒ์ผ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
        filename: safeFilename 
      });
      
    } catch (error) {
      console.error('์—…๋กœ๋“œ ์˜ค๋ฅ˜:', error);
      res.status(400).json({ error: error.message });
    }
  }
);

๐ŸŒ ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ์—์„œ์˜ ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ

์š”์ฆ˜์€ ๋Œ€๋ถ€๋ถ„ ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ์—์„œ ์„œ๋น„์Šค๋ฅผ ์šด์˜ํ•˜์ž–์•„? AWS, Azure, GCP ๊ฐ™์€ ๊ณณ ๋ง์ด์•ผ. ํด๋ผ์šฐ๋“œ์—์„œ๋Š” ๋˜ ๋‹ค๋ฅธ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ๋“ค์ด ์žˆ์–ด! โ˜๏ธ

AWS S3๋ฅผ ํ™œ์šฉํ•œ ์•ˆ์ „ํ•œ ํŒŒ์ผ ์—…๋กœ๋“œ

AWS S3๋Š” ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋ณด์•ˆ ๊ธฐ๋Šฅ๋“ค์„ ์ œ๊ณตํ•ด. ์ œ๋Œ€๋กœ๋งŒ ์„ค์ •ํ•˜๋ฉด ๊ฑฐ์˜ ์™„๋ฒฝํ•œ ๋ณด์•ˆ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด!
โ˜๏ธ AWS S3 ๋ณด์•ˆ ์„ค์ • ์™„๋ฒฝ ๊ฐ€์ด๋“œ
const AWS = require('aws-sdk');
const crypto = require('crypto');

// S3 ํด๋ผ์ด์–ธํŠธ ์„ค์ •
const s3 = new AWS.S3({
  accessKeyId: process.env.AWS_ACCESS_KEY,
  secretAccessKey: process.env.AWS_SECRET_KEY,
  region: process.env.AWS_REGION,
  signatureVersion: 'v4' // ์ตœ์‹  ์„œ๋ช… ๋ฒ„์ „ ์‚ฌ์šฉ
});

// ๋ฒ„ํ‚ท ์ •์ฑ… ์„ค์ • (JSON)
const bucketPolicy = {
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::your-bucket-name/*",
        "arn:aws:s3:::your-bucket-name"
      ],
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "false" // HTTPS๋งŒ ํ—ˆ์šฉ
        }
      }
    },
    {
      "Sid": "DenyUnencryptedObjectUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::your-bucket-name/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "AES256" // ์•”ํ˜ธํ™” ๊ฐ•์ œ
        }
      }
    }
  ]
};

// ์•ˆ์ „ํ•œ ์—…๋กœ๋“œ ํ•จ์ˆ˜
async function secureS3Upload(file, userId) {
  // 1. ํŒŒ์ผ ๊ฒ€์ฆ (์ด์ „์— ๋งŒ๋“  ํ•จ์ˆ˜๋“ค ์‚ฌ์šฉ)
  await validateFile(file);
  
  // 2. ์•ˆ์ „ํ•œ ํŒŒ์ผ๋ช… ์ƒ์„ฑ
  const safeFilename = generateSecureFilename(file.originalname);
  const s3Key = `users/${userId}/${safeFilename}`;
  
  // 3. S3 ์—…๋กœ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ
  const params = {
    Bucket: process.env.S3_BUCKET,
    Key: s3Key,
    Body: file.buffer,
    ContentType: file.mimetype,
    
    // ๋ณด์•ˆ ์„ค์ •
    ACL: 'private', // ์ ˆ๋Œ€ public์œผ๋กœ ํ•˜์ง€ ๋งˆ!
    ServerSideEncryption: 'AES256', // ์„œ๋ฒ„ ์ธก ์•”ํ˜ธํ™”
    
    // ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
    Metadata: {
      'user-id': userId.toString(),
      'original-name': Buffer.from(file.originalname).toString('base64'),
      'upload-date': new Date().toISOString(),
      'content-hash': crypto.createHash('sha256').update(file.buffer).digest('hex')
    },
    
    // ํƒœ๊ทธ (๋น„์šฉ ์ถ”์  ๋ฐ ๊ด€๋ฆฌ์šฉ)
    Tagging: `user-id=${userId}&upload-date=${Date.now()}`,
    
    // ์Šคํ† ๋ฆฌ์ง€ ํด๋ž˜์Šค (๋น„์šฉ ์ตœ์ ํ™”)
    StorageClass: 'STANDARD_IA' // ์ž์ฃผ ์ ‘๊ทผํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ์šฉ
  };
  
  // 4. ์—…๋กœ๋“œ ์‹คํ–‰
  const result = await s3.upload(params).promise();
  
  return {
    key: s3Key,
    etag: result.ETag,
    location: result.Location
  };
}

// Presigned URL๋กœ ์•ˆ์ „ํ•œ ๋‹ค์šด๋กœ๋“œ
async function generateSecureDownloadUrl(s3Key, expiresIn = 3600) {
  const params = {
    Bucket: process.env.S3_BUCKET,
    Key: s3Key,
    Expires: expiresIn, // 1์‹œ๊ฐ„ ํ›„ ๋งŒ๋ฃŒ
    
    // ์‘๋‹ต ํ—ค๋” ๊ฐ•์ œ ์„ค์ •
    ResponseContentType: 'application/octet-stream',
    ResponseContentDisposition: 'attachment',
    
    // ๋ณด์•ˆ ํ—ค๋”
    ResponseCacheControl: 'no-cache, no-store, must-revalidate',
    ResponseContentSecurityPolicy: "default-src 'none'"
  };
  
  return s3.getSignedUrlPromise('getObject', params);
}

// S3 ๋ฒ„ํ‚ท CORS ์„ค์ •
const corsConfiguration = {
  CORSRules: [
    {
      AllowedOrigins: ['https://yourdomain.com'], // ํŠน์ • ๋„๋ฉ”์ธ๋งŒ ํ—ˆ์šฉ
      AllowedMethods: ['GET', 'PUT', 'POST'],
      AllowedHeaders: ['*'],
      MaxAgeSeconds: 3000,
      ExposeHeaders: ['ETag']
    }
  ]
};

// ๋ผ์šฐํŠธ์—์„œ ์‚ฌ์šฉ
app.post('/upload', 
  authenticate, // ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด
  upload.single('file'),
  async (req, res) => {
    try {
      const result = await secureS3Upload(req.file, req.user.id);
      
      // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๊ธฐ๋ก
      await db.query(`
        INSERT INTO files (user_id, s3_key, filename, file_size, etag)
        VALUES (?, ?, ?, ?, ?)
      `, [
        req.user.id,
        result.key,
        req.file.originalname,
        req.file.size,
        result.etag
      ]);
      
      res.json({
        message: 'ํŒŒ์ผ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
        fileId: result.key
      });
      
    } catch (error) {
      console.error('S3 ์—…๋กœ๋“œ ์˜ค๋ฅ˜:', error);
      res.status(500).json({ error: '์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.' });
    }
  }
);

// ๋‹ค์šด๋กœ๋“œ ๋ผ์šฐํŠธ
app.get('/download/:fileId',
  authenticate,
  async (req, res) => {
    try {
      // ํŒŒ์ผ ์†Œ์œ ๊ถŒ ํ™•์ธ
      const file = await db.query(
        'SELECT * FROM files WHERE s3_key = ? AND user_id = ?',
        [req.params.fileId, req.user.id]
      );
      
      if (!file) {
        return res.status(404).json({ error: 'ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' });
      }
      
      // Presigned URL ์ƒ์„ฑ
      const downloadUrl = await generateSecureDownloadUrl(file.s3_key);
      
      res.json({ downloadUrl });
      
    } catch (error) {
      console.error('๋‹ค์šด๋กœ๋“œ ์˜ค๋ฅ˜:', error);
      res.status(500).json({ error: '๋‹ค์šด๋กœ๋“œ URL ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.' });
    }
  }
);
โœ… S3 ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ โ€ข โœ… ๋ฒ„ํ‚ท์„ ์ ˆ๋Œ€ public์œผ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š๊ธฐ
โ€ข โœ… ์„œ๋ฒ„ ์ธก ์•”ํ˜ธํ™”(SSE) ํ™œ์„ฑํ™”
โ€ข โœ… HTTPS๋งŒ ํ—ˆ์šฉํ•˜๋„๋ก ๋ฒ„ํ‚ท ์ •์ฑ… ์„ค์ •
โ€ข โœ… Presigned URL๋กœ๋งŒ ํŒŒ์ผ ์ ‘๊ทผ ํ—ˆ์šฉ
โ€ข โœ… IAM ์—ญํ• ๋กœ ์ตœ์†Œ ๊ถŒํ•œ ์›์น™ ์ ์šฉ
โ€ข โœ… S3 ๋ฒ„ํ‚ท ๋ฒ„์ „ ๊ด€๋ฆฌ ํ™œ์„ฑํ™”
โ€ข โœ… CloudTrail๋กœ ๋ชจ๋“  ์ ‘๊ทผ ๋กœ๊น…
โ€ข โœ… ์ˆ˜๋ช… ์ฃผ๊ธฐ ์ •์ฑ…์œผ๋กœ ์˜ค๋ž˜๋œ ํŒŒ์ผ ์ž๋™ ์‚ญ์ œ

์ง์ ‘ ์—…๋กœ๋“œ(Direct Upload) ํŒจํ„ด

์„œ๋ฒ„๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ํด๋ผ์ด์–ธํŠธ์—์„œ S3๋กœ ์ง์ ‘ ์—…๋กœ๋“œํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์–ด. ์„œ๋ฒ„ ๋ถ€ํ•˜๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ๊ณ  ๋” ๋น ๋ฅด์ง€! ๐Ÿš€
๐Ÿš€ Direct Upload ๊ตฌํ˜„
// ์„œ๋ฒ„: Presigned POST URL ์ƒ์„ฑ
app.post('/get-upload-url', authenticate, async (req, res) => {
  const { filename, contentType } = req.body;
  
  // ํŒŒ์ผ ๊ฒ€์ฆ
  const ext = path.extname(filename).toLowerCase();
  if (!['.jpg', '.png', '.pdf'].includes(ext)) {
    return res.status(400).json({ error: 'ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค.' });
  }
  
  const safeFilename = generateSecureFilename(filename);
  const s3Key = `users/${req.user.id}/${safeFilename}`;
  
  const params = {
    Bucket: process.env.S3_BUCKET,
    Fields: {
      key: s3Key,
      'Content-Type': contentType,
      'x-amz-server-side-encryption': 'AES256',
      'x-amz-meta-user-id': req.user.id.toString()
    },
    Conditions: [
      ['content-length-range', 0, 5 * 1024 * 1024], // ์ตœ๋Œ€ 5MB
      ['eq', '$Content-Type', contentType],
      ['starts-with', '$key', `users/${req.user.id}/`]
    ],
    Expires: 300 // 5๋ถ„ ํ›„ ๋งŒ๋ฃŒ
  };
  
  s3.createPresignedPost(params, (err, data) => {
    if (err) {
      return res.status(500).json({ error: '์—…๋กœ๋“œ URL ์ƒ์„ฑ ์‹คํŒจ' });
    }
    
    res.json({
      uploadUrl: data.url,
      fields: data.fields,
      key: s3Key
    });
  });
});

// ํด๋ผ์ด์–ธํŠธ: ์ง์ ‘ ์—…๋กœ๋“œ
async function directUploadToS3(file) {
  // 1. ์„œ๋ฒ„์—์„œ Presigned URL ๋ฐ›๊ธฐ
  const response = await fetch('/get-upload-url', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify({
      filename: file.name,
      contentType: file.type
    })
  });
  
  const { uploadUrl, fields, key } = await response.json();
  
  // 2. FormData ์ƒ์„ฑ
  const formData = new FormData();
  Object.entries(fields).forEach(([key, value]) => {
    formData.append(key, value);
  });
  formData.append('file', file);
  
  // 3. S3๋กœ ์ง์ ‘ ์—…๋กœ๋“œ
  const uploadResponse = await fetch(uploadUrl, {
    method: 'POST',
    body: formData
  });
  
  if (!uploadResponse.ok) {
    throw new Error('์—…๋กœ๋“œ ์‹คํŒจ');
  }
  
  // 4. ์„œ๋ฒ„์— ์—…๋กœ๋“œ ์™„๋ฃŒ ์•Œ๋ฆผ
  await fetch('/upload-complete', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify({
      key: key,
      filename: file.name,
      size: file.size
    })
  });
  
  return key;
}

๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ์˜ ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ

๋ชจ๋ฐ”์ผ ์•ฑ๋„ ์›น๊ณผ ๋น„์Šทํ•˜์ง€๋งŒ, ๋ช‡ ๊ฐ€์ง€ ์ถ”๊ฐ€ ๊ณ ๋ ค์‚ฌํ•ญ์ด ์žˆ์–ด!

iOS/Android ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ

๐Ÿ“ฑ iOS Swift ์•ˆ์ „ํ•œ ์—…๋กœ๋“œ
import UIKit
import CryptoKit

class SecureFileUploader {
    
    // ํ—ˆ์šฉ๋œ ํŒŒ์ผ ํ˜•์‹
    let allowedMimeTypes = [
        "image/jpeg",
        "image/png",
        "image/gif",
        "application/pdf"
    ]
    
    let maxFileSize = 5 * 1024 * 1024 // 5MB
    
    func uploadFile(fileURL: URL, completion: @escaping (Result<String, Error>) -> Void) {
        
        // 1. ํŒŒ์ผ ํฌ๊ธฐ ๊ฒ€์ฆ
        guard let fileSize = try? FileManager.default.attributesOfItem(atPath: fileURL.path)[.size] as? Int,
              fileSize <= maxFileSize else {
            completion(.failure(UploadError.fileTooLarge))
            return
        }
        
        // 2. MIME ํƒ€์ž… ๊ฒ€์ฆ
        guard let mimeType = getMimeType(for: fileURL),
              allowedMimeTypes.contains(mimeType) else {
            completion(.failure(UploadError.invalidFileType))
            return
        }
        
        // 3. ํŒŒ์ผ ํ•ด์‹œ ๊ณ„์‚ฐ (๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ์šฉ)
        guard let fileData = try? Data(contentsOf: fileURL),
              let fileHash = calculateSHA256(data: fileData) else {
            completion(.failure(UploadError.fileReadError))
            return
        }
        
        // 4. ์„œ๋ฒ„๋กœ ์—…๋กœ๋“œ
        uploadToServer(fileData: fileData, mimeType: mimeType, hash: fileHash, completion: completion)
    }
    
    private func getMimeType(for url: URL) -> String? {
        let pathExtension = url.pathExtension.lowercased()
        
        switch pathExtension {
        case "jpg", "jpeg":
            return "image/jpeg"
        case "png":
            return "image/png"
        case "gif":
            return "image/gif"
        case "pdf":
            return "application/pdf"
        default:
            return nil
        }
    }
    
    private func calculateSHA256(data: Data) -> String? {
        let hash = SHA256.hash(data: data)
        return hash.compactMap { String(format: "%02x", $0) }.joined()
    }
    
    private func uploadToServer(fileData: Data, mimeType: String, hash: String, 
                               completion: @escaping (Result<String, Error>) -> Void) {
        
        guard let url = URL(string: "https://api.example.com/upload") else {
            completion(.failure(UploadError.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
        
        // Multipart form data ์ƒ์„ฑ
        let boundary = UUID().uuidString
        request.setValue("multipart/form-data; boundary=\(boundary)", 
                        forHTTPHeaderField: "Content-Type")
        
        var body = Data()
        
        // ํŒŒ์ผ ํ•ด์‹œ ์ถ”๊ฐ€
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"hash\"\r\n\r\n".data(using: .utf8)!)
        body.append("\(hash)\r\n".data(using: .utf8)!)
        
        // ํŒŒ์ผ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"file\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
        body.append(fileData)
        body.append("\r\n".data(using: .utf8)!)
        body.append("--\(boundary)--\r\n".data(using: .utf8)!)
        
        request.httpBody = body
        
        // ์—…๋กœ๋“œ ์‹คํ–‰
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let httpResponse = response as? HTTPURLResponse,
                  (200...299).contains(httpResponse.statusCode) else {
                completion(.failure(UploadError.serverError))
                return
            }
            
            // ์„ฑ๊ณต
            completion(.success("Upload successful"))
        }.resume()
    }
}

enum UploadError: Error {
    case fileTooLarge
    case invalidFileType
    case fileReadError
    case invalidURL
    case serverError
}
๐Ÿ“ฑ Android Kotlin ์•ˆ์ „ํ•œ ์—…๋กœ๋“œ
import android.content.Context
import android.net.Uri
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.security.MessageDigest

class SecureFileUploader(private val context: Context) {
    
    private val allowedMimeTypes = setOf(
        "image/jpeg",
        "image/png",
        "image/gif",
        "application/pdf"
    )
    
    private val maxFileSize = 5 * 1024 * 1024L // 5MB
    
    fun uploadFile(uri: Uri, callback: (Result<String>) -> Unit) {
        try {
            // 1. ํŒŒ์ผ ํฌ๊ธฐ ๊ฒ€์ฆ
            val fileSize = getFileSize(uri)
            if (fileSize > maxFileSize) {
                callback(Result.failure(Exception("ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค")))
                return
            }
            
            // 2. MIME ํƒ€์ž… ๊ฒ€์ฆ
            val mimeType = context.contentResolver.getType(uri)
            if (mimeType !in allowedMimeTypes) {
                callback(Result.failure(Exception("ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค")))
                return
            }
            
            // 3. ํŒŒ์ผ ์ฝ๊ธฐ
            val inputStream = context.contentResolver.openInputStream(uri)
            val file = File(context.cacheDir, "temp_upload")
            inputStream?.use { input ->
                file.outputStream().use { output ->
                    input.copyTo(output)
                }
            }
            
            // 4. ํŒŒ์ผ ํ•ด์‹œ ๊ณ„์‚ฐ
            val fileHash = calculateSHA256(file)
            
            // 5. ์„œ๋ฒ„๋กœ ์—…๋กœ๋“œ
            uploadToServer(file, mimeType!!, fileHash, callback)
            
        } catch (e: Exception) {
            callback(Result.failure(e))
        }
    }
    
    private fun getFileSize(uri: Uri): Long {
        context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
            val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
            cursor.moveToFirst()
            return cursor.getLong(sizeIndex)
        }
        return 0
    }
    
    private fun calculateSHA256(file: File): String {
        val digest = MessageDigest.getInstance("SHA-256")
        val bytes = file.readBytes()
        val hash = digest.digest(bytes)
        return hash.joinToString("") { "%02x".format(it) }
    }
    
    private fun uploadToServer(file: File, mimeType: String, hash: String, 
                              callback: (Result<String>) -> Unit) {
        
        val client = OkHttpClient()
        
        val requestBody = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("hash", hash)
            .addFormDataPart(
                "file",
                file.name,
                file.asRequestBody(mimeType.toMediaType())
            )
            .build()
        
        val request = Request.Builder()
            .url("https://api.example.com/upload")
            .addHeader("Authorization", "Bearer $authToken")
            .post(requestBody)
            .build()
        
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                callback(Result.failure(e))
            }
            
            override fun onResponse(call: Call, response: Response) {
                if (response.isSuccessful) {
                    callback(Result.success("์—…๋กœ๋“œ ์„ฑ๊ณต"))
                } else {
                    callback(Result.failure(Exception("์„œ๋ฒ„ ์˜ค๋ฅ˜: ${response.code}")))
                }
                
                // ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
                file.delete()
            }
        })
    }
}

๐ŸŽฏ ์žฌ๋Šฅ๋„ท์—์„œ ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ ๊ตฌํ˜„ํ•˜๊ธฐ

์ž, ์ด์ œ ์‹ค์ œ ์„œ๋น„์Šค์— ์ ์šฉํ•ด๋ณด์ž! ์žฌ๋Šฅ๋„ท(https://www.jaenung.net) ๊ฐ™์€ ์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ์—์„œ๋Š” ๋‹ค์–‘ํ•œ ์ข…๋ฅ˜์˜ ํŒŒ์ผ์ด ์—…๋กœ๋“œ๋˜์ž–์•„? ํฌํŠธํด๋ฆฌ์˜ค ์ด๋ฏธ์ง€, ์ž‘์—… ํŒŒ์ผ, ๊ณ„์•ฝ์„œ ๋“ฑ๋“ฑ... ๐ŸŽจ๐Ÿ“„

์žฌ๋Šฅ๋„ท์—์„œ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ๋‹ค๋ฉด ์ด๋ ‡๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด:
1 ์‚ฌ์šฉ์ž ์œ ํ˜•๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ๋ณด์•ˆ ์ •์ฑ…

โ€ข ์ผ๋ฐ˜ ํšŒ์›: ์ด๋ฏธ์ง€๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅ (ํ”„๋กœํ•„, ํฌํŠธํด๋ฆฌ์˜ค)
โ€ข ์ „๋ฌธ๊ฐ€ ํšŒ์›: ์ด๋ฏธ์ง€ + ๋ฌธ์„œ ํŒŒ์ผ (PDF, DOCX)
โ€ข ํ”„๋ฆฌ๋ฏธ์—„ ํšŒ์›: ๋” ํฐ ํŒŒ์ผ ํฌ๊ธฐ ํ—ˆ์šฉ
โ€ข ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํŒŒ์ผ ํ˜•์‹ ํ—ˆ์šฉ (๋‹จ, ๋ณ„๋„ ๊ฒ€์ฆ)
๐ŸŽฏ ์žฌ๋Šฅ๋„ท ๋งž์ถคํ˜• ์—…๋กœ๋“œ ์‹œ์Šคํ…œ
// ์‚ฌ์šฉ์ž ๋“ฑ๊ธ‰๋ณ„ ์„ค์ •
const USER_UPLOAD_POLICIES = {
  basic: {
    allowedTypes: ['.jpg', '.jpeg', '.png', '.gif'],
    maxFileSize: 2 * 1024 * 1024, // 2MB
    maxFiles: 5,
    dailyLimit: 10
  },
  expert: {
    allowedTypes: ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.docx'],
    maxFileSize: 10 * 1024 * 1024, // 10MB
    maxFiles: 20,
    dailyLimit: 50
  },
  premium: {
    allowedTypes: ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.docx', '.zip'],
    maxFileSize: 50 * 1024 * 1024, // 50MB
    maxFiles: 100,
    dailyLimit: 200
  }
};

// ์žฌ๋Šฅ๋„ท ์ „์šฉ ์—…๋กœ๋“œ ๋ฏธ๋“ค์›จ์–ด
function jaeningNetUploadMiddleware(req, res, next) {
  const userTier = req.user.tier; // 'basic', 'expert', 'premium'
  const policy = USER_UPLOAD_POLICIES[userTier];
  
  // ๋™์ ์œผ๋กœ multer ์„ค์ • ์ƒ์„ฑ
  const upload = multer({
    storage: multer.memoryStorage(),
    limits: {
      fileSize: policy.maxFileSize,
      files: policy.maxFiles
    },
    fileFilter: (req, file, cb) => {
      const ext = path.extname(file.originalname).toLowerCase();
      
      if (!policy.allowedTypes.includes(ext)) {
        return cb(new Error(`${userTier} ๋“ฑ๊ธ‰์€ ${ext} ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`));
      }
      
      cb(null, true);
    }
  }).array('files', policy.maxFiles);
  
  upload(req, res, next);
}

// ์ผ์ผ ์—…๋กœ๋“œ ์ œํ•œ ์ฒดํฌ
async function checkDailyLimit(userId, userTier) {
  const today = new Date().toISOString().split('T')[0];
  
  const count = await db.query(`
    SELECT COUNT(*) as count 
    FROM uploads 
    WHERE user_id = ? AND DATE(created_at) = ?
  `, [userId, today]);
  
  const policy = USER_UPLOAD_POLICIES[userTier];
  
  if (count[0].count >= policy.dailyLimit) {
    throw new Error(`์ผ์ผ ์—…๋กœ๋“œ ํ•œ๋„(${policy.dailyLimit}๊ฐœ)๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค.`);
  }
}

// ํฌํŠธํด๋ฆฌ์˜ค ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
app.post('/api/portfolio/upload',
  authenticate,
  jaeningNetUploadMiddleware,
  async (req, res) => {
    try {
      // ์ผ์ผ ํ•œ๋„ ์ฒดํฌ
      await checkDailyLimit(req.user.id, req.user.tier);
      
      const uploadedFiles = [];
      
      for (const file of req.files) {
        // ๋ณด์•ˆ ๊ฒ€์ฆ
        const isValid = await validateFile(file);
        if (!isValid) {
          continue; // ์œ ํšจํ•˜์ง€ ์•Š์€ ํŒŒ์ผ์€ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
        }
        
        // ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๋ฐ ์›Œํ„ฐ๋งˆํฌ ์ถ”๊ฐ€
        const processedImage = await processPortfolioImage(file);
        
        // S3 ์—…๋กœ๋“œ
        const s3Key = await uploadToS3(processedImage, req.user.id);
        
        // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ
        const fileRecord = await db.query(`
          INSERT INTO portfolio_images 
          (user_id, s3_key, original_name, file_size, tier)
          VALUES (?, ?, ?, ?, ?)
        `, [
          req.user.id,
          s3Key,
          file.originalname,
          file.size,
          req.user.tier
        ]);
        
        uploadedFiles.push({
          id: fileRecord.insertId,
          url: await generateThumbnailUrl(s3Key)
        });
      }
      
      res.json({
        message: 'ํฌํŠธํด๋ฆฌ์˜ค ์ด๋ฏธ์ง€๊ฐ€ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
        files: uploadedFiles,
        remainingQuota: USER_UPLOAD_POLICIES[req.user.tier].dailyLimit - uploadedFiles.length
      });
      
    } catch (error) {
      console.error('์—…๋กœ๋“œ ์˜ค๋ฅ˜:', error);
      res.status(400).json({ error: error.message });
    }
  }
);

// ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ (์›Œํ„ฐ๋งˆํฌ, ๋ฆฌ์‚ฌ์ด์ง•)
async function processPortfolioImage(file) {
  return sharp(file.buffer)
    .resize(1920, 1080, {
      fit: 'inside',
      withoutEnlargement: true
    })
    .composite([{
      input: Buffer.from(`
        <svg width="200" height="50">
          <text x="10" y="30" font-family="Arial" font-size="20" 
                fill="rgba(255,255,255,0.5)">์žฌ๋Šฅ๋„ท</text>
        </svg>
      `),
      gravity: 'southeast'
    }])
    .jpeg({ quality: 85 })
    .toBuffer();
}
๐Ÿ’ก ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ถ”๊ฐ€๋กœ ๊ณ ๋ คํ•  ์‚ฌํ•ญ 1. ์ €์ž‘๊ถŒ ๋ณดํ˜ธ: ์—…๋กœ๋“œ๋œ ํฌํŠธํด๋ฆฌ์˜ค ์ด๋ฏธ์ง€์— ์ž๋™์œผ๋กœ ์›Œํ„ฐ๋งˆํฌ ์ถ”๊ฐ€
2. ์ด๋ฏธ์ง€ ํ’ˆ์งˆ ๊ด€๋ฆฌ: ๋„ˆ๋ฌด ์ €ํ™”์งˆ ์ด๋ฏธ์ง€๋Š” ์ž๋™ ๊ฑฐ๋ถ€
3. ์„ฑ์ธ ์ปจํ…์ธ  ํ•„ํ„ฐ๋ง: AI ๊ธฐ๋ฐ˜ ๋ถ€์ ์ ˆํ•œ ์ด๋ฏธ์ง€ ํƒ์ง€
4. ์ค‘๋ณต ํŒŒ์ผ ๊ฐ์ง€: ๊ฐ™์€ ํŒŒ์ผ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์—…๋กœ๋“œํ•˜๋Š” ๊ฒƒ ๋ฐฉ์ง€
5. ์ž๋™ ์ธ๋„ค์ผ ์ƒ์„ฑ: ๋‹ค์–‘ํ•œ ํฌ๊ธฐ์˜ ์ธ๋„ค์ผ ์ž๋™ ์ƒ์„ฑ

๐Ÿ” ๋ณด์•ˆ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น…

ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ์—์„œ ์ •๋ง ์ค‘์š”ํ•œ ๊ฒŒ ํ•˜๋‚˜ ๋” ์žˆ์–ด. ๋ฐ”๋กœ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…์ด์•ผ! ๊ณต๊ฒฉ์„ ๋ง‰๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•˜์ง€๋งŒ, ๊ณต๊ฒฉ ์‹œ๋„๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ์ถ”์ ํ•˜๋Š” ๊ฒƒ๋„ ๋˜‘๊ฐ™์ด ์ค‘์š”ํ•˜๊ฑฐ๋“ . ๐Ÿ•ต๏ธ

ํฌ๊ด„์ ์ธ ๋กœ๊น… ์‹œ์Šคํ…œ

๐Ÿ“Š ์™„๋ฒฝํ•œ ๋กœ๊น… ์‹œ์Šคํ…œ
const winston = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');

// Winston ๋กœ๊ฑฐ ์„ค์ •
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    // ํŒŒ์ผ ๋กœ๊ทธ
    new winston.transports.File({ 
      filename: 'logs/upload-error.log', 
      level: 'error' 
    }),
    new winston.transports.File({ 
      filename: 'logs/upload-combined.log' 
    }),
    
    // Elasticsearch (์„ ํƒ์‚ฌํ•ญ)
    new ElasticsearchTransport({
      level: 'info',
      clientOpts: { node: 'http://localhost:9200' },
      index: 'upload-logs'
    })
  ]
});

// ์—…๋กœ๋“œ ์ด๋ฒคํŠธ ๋กœ๊น… ๋ฏธ๋“ค์›จ์–ด
function uploadLoggingMiddleware(req, res, next) {
  const startTime = Date.now();
  
  // ์‘๋‹ต ์™„๋ฃŒ ์‹œ ๋กœ๊ทธ ๊ธฐ๋ก
  res.on('finish', () => {
    const duration = Date.now() - startTime;
    
    const logData = {
      timestamp: new Date().toISOString(),
      userId: req.user?.id || 'anonymous',
      ip: req.ip,
      userAgent: req.get('user-agent'),
      method: req.method,
      url: req.originalUrl,
      statusCode: res.statusCode,
      duration: duration,
      fileInfo: req.file ? {
        originalName: req.file.originalname,
        mimeType: req.file.mimetype,
        size: req.file.size,
        encoding: req.file.encoding
      } : null,
      error: res.locals.error || null
    };
    
    if (res.statusCode >= 400) {
      logger.error('Upload failed', logData);
    } else {
      logger.info('Upload successful', logData);
    }
  });
  
  next();
}

// ์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™ ๊ฐ์ง€
class SecurityMonitor {
  constructor() {
    this.suspiciousActivities = new Map();
    this.blockedIPs = new Set();
  }
  
  // ์˜์‹ฌ์Šค๋Ÿฌ์šด ํŒจํ„ด ๊ฐ์ง€
  detectSuspiciousActivity(userId, ip, activity) {
    const key = `${userId}-${ip}`;
    
    if (!this.suspiciousActivities.has(key)) {
      this.suspiciousActivities.set(key, []);
    }
    
    const activities = this.suspiciousActivities.get(key);
    activities.push({
      activity,
      timestamp: Date.now()
    });
    
    // ์ตœ๊ทผ 5๋ถ„๊ฐ„์˜ ํ™œ๋™๋งŒ ์œ ์ง€
    const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
    const recentActivities = activities.filter(a => a.timestamp > fiveMinutesAgo);
    this.suspiciousActivities.set(key, recentActivities);
    
    // ์˜์‹ฌ์Šค๋Ÿฌ์šด ํŒจํ„ด ์ฒดํฌ
    if (this.isSuspicious(recentActivities)) {
      this.blockIP(ip);
      this.alertAdmin(userId, ip, recentActivities);
      return true;
    }
    
    return false;
  }
  
  isSuspicious(activities) {
    // 5๋ถ„ ๋‚ด์— 10๋ฒˆ ์ด์ƒ ์‹คํŒจ
    const failures = activities.filter(a => a.activity === 'upload_failed');
    if (failures.length >= 10) {
      return true;
    }
    
    // ๋‹ค์–‘ํ•œ ํ™•์žฅ์ž๋กœ ์—…๋กœ๋“œ ์‹œ๋„
    const extensions = new Set(
      activities
        .filter(a => a.activity === 'invalid_extension')
        .map(a => a.extension)
    );
    if (extensions.size >= 5) {
      return true;
    }
    
    // ์งง์€ ์‹œ๊ฐ„์— ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์—ฌ๋Ÿฌ ๊ฐœ
    const largeFiles = activities.filter(a => 
      a.activity === 'large_file' && a.size > 10 * 1024 * 1024
    );
    if (largeFiles.length >= 5) {
      return true;
    }
    
    return false;
  }
  
  blockIP(ip) {
    this.blockedIPs.add(ip);
    logger.warn('IP blocked due to suspicious activity', { ip });
    
    // 1์‹œ๊ฐ„ ํ›„ ์ž๋™ ํ•ด์ œ
    setTimeout(() => {
      this.blockedIPs.delete(ip);
      logger.info('IP unblocked', { ip });
    }, 60 * 60 * 1000);
  }
  
  isBlocked(ip) {
    return this.blockedIPs.has(ip);
  }
  
  async alertAdmin(userId, ip, activities) {
    // ๊ด€๋ฆฌ์ž์—๊ฒŒ ์•Œ๋ฆผ ์ „์†ก
    await sendAdminAlert({
      type: 'SUSPICIOUS_UPLOAD_ACTIVITY',
      userId,
      ip,
      activities,
      timestamp: new Date().toISOString()
    });
    
    logger.error('Suspicious activity detected', {
      userId,
      ip,
      activityCount: activities.length
    });
  }
}

const securityMonitor = new SecurityMonitor();

// ๋ณด์•ˆ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฏธ๋“ค์›จ์–ด
function securityMonitoringMiddleware(req, res, next) {
  // IP ์ฐจ๋‹จ ์ฒดํฌ
  if (securityMonitor.isBlocked(req.ip)) {
    logger.warn('Blocked IP attempted access', { ip: req.ip });
    return res.status(403).json({ 
      error: '์ ‘๊ทผ์ด ์ฐจ๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.' 
    });
  }
  
  next();
}

// ์—…๋กœ๋“œ ๋ผ์šฐํŠธ์— ๋ชจ๋“  ๋ณด์•ˆ ๊ณ„์ธต ์ ์šฉ
app.post('/upload',
  uploadLoggingMiddleware,
  securityMonitoringMiddleware,
  authenticate,
  uploadLimiter,
  upload.single('file'),
  async (req, res) => {
    try {
      // ํŒŒ์ผ ๊ฒ€์ฆ
      if (!req.file) {
        securityMonitor.detectSuspiciousActivity(
          req.user.id,
          req.ip,
          { activity: 'no_file' }
        );
        throw new Error('ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.');
      }
      
      // ํ™•์žฅ์ž ๊ฒ€์ฆ
      const ext = path.extname(req.file.originalname).toLowerCase();
      if (!ALLOWED_EXTENSIONS.includes(ext)) {
        securityMonitor.detectSuspiciousActivity(
          req.user.id,
          req.ip,
          { activity: 'invalid_extension', extension: ext }
        );
        throw new Error('ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค.');
      }
      
      // ํŒŒ์ผ ํฌ๊ธฐ ์ฒดํฌ
      if (req.file.size > MAX_FILE_SIZE) {
        securityMonitor.detectSuspiciousActivity(
          req.user.id,
          req.ip,
          { activity: 'large_file', size: req.file.size }
        );
        throw new Error('ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค.');
      }
      
      // ๋งค์ง ๋ฐ”์ดํŠธ ๊ฒ€์ฆ
      const isValid = await verifyFileType(req.file.path, ext.slice(1));
      if (!isValid) {
        securityMonitor.detectSuspiciousActivity(
          req.user.id,
          req.ip,
          { activity: 'magic_byte_mismatch', extension: ext }
        );
        await fs.unlink(req.file.path);
        throw new Error('ํŒŒ์ผ ํ˜•์‹์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
      }
      
      // ์•ˆ์ „ํ•œ ์ฒ˜๋ฆฌ
      const safeFilename = await secureFileUpload(req.file);
      
      logger.info('File uploaded successfully', {
        userId: req.user.id,
        filename: safeFilename,
        originalName: req.file.originalname,
        size: req.file.size
      });
      
      res.json({ 
        message: 'ํŒŒ์ผ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
        filename: safeFilename 
      });
      
    } catch (error) {
      res.locals.error = error.message;
      
      securityMonitor.detectSuspiciousActivity(
        req.user?.id || 'anonymous',
        req.ip,
        { activity: 'upload_failed', error: error.message }
      );
      
      res.status(400).json({ error: error.message });
    }
  }
);

์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ

๋กœ๊ทธ๋งŒ ์Œ“์•„๋†“์œผ๋ฉด ๋ญํ•ด? ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์–ด์•ผ์ง€! ๊ฐ„๋‹จํ•œ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.
๐Ÿ“Š ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง ๋Œ€์‹œ๋ณด๋“œ API
// ํ†ต๊ณ„ API
app.get('/api/admin/upload-stats', authenticateAdmin, async (req, res) => {
  try {
    const stats = await db.query(`
      SELECT 
        DATE(created_at) as date,
        COUNT(*) as total_uploads,
        SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful,
        SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
        SUM(file_size) as total_size,
        AVG(file_size) as avg_size
      FROM uploads
      WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
      GROUP BY DATE(created_at)
      ORDER BY date DESC
    `);
    
    const topUsers = await db.query(`
      SELECT 
        user_id,
        COUNT(*) as upload_count,
        SUM(file_size) as total_size
      FROM uploads
      WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
      GROUP BY user_id
      ORDER BY upload_count DESC
      LIMIT 10
    `);
    
    const suspiciousActivities = await db.query(`
      SELECT *
      FROM security_logs
      WHERE severity = 'high'
        AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
      ORDER BY created_at DESC
      LIMIT 20
    `);
    
    res.json({
      stats,
      topUsers,
      suspiciousActivities,
      blockedIPs: Array.from(securityMonitor.blockedIPs)
    });
    
  } catch (error) {
    console.error('ํ†ต๊ณ„ ์กฐํšŒ ์˜ค๋ฅ˜:', error);
    res.status(500).json({ error: 'ํ†ต๊ณ„๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' });
  }
});

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌ: ๋ณด์•ˆ์€ ์—ฌ์ •์ด์ง€ ๋ชฉ์ ์ง€๊ฐ€ ์•„๋‹ˆ์•ผ

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด! ๐Ÿ˜… ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ์— ๋Œ€ํ•ด ๊ฑฐ์˜ ๋ชจ๋“  ๊ฑธ ๋‹ค๋ค˜๋‹ค๊ณ  ๋ด๋„ ๋ผ. ํ•˜์ง€๋งŒ ๊ธฐ์–ตํ•ด์ค˜, ๋ณด์•ˆ์€ ํ•œ ๋ฒˆ ๊ตฌํ˜„ํ•˜๊ณ  ๋๋‚˜๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ. ๊ณ„์†ํ•ด์„œ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ๊ฐœ์„ ํ•ด์•ผ ํ•˜๋Š” ๊ฑฐ์ง€.

โœจ ํ•ต์‹ฌ ์š”์•ฝ: ๋ฐ˜๋“œ์‹œ ๊ธฐ์–ตํ•ด์•ผ ํ•  ๊ฒƒ๋“ค 1. ๋‹ค์ธต ๋ฐฉ์–ด (Defense in Depth)
ํ•˜๋‚˜์˜ ๋ณด์•ˆ ์žฅ์น˜๋งŒ ๋ฏฟ์ง€ ๋งˆ. ์—ฌ๋Ÿฌ ๊ฒน์˜ ๋ณด์•ˆ ๊ณ„์ธต์„ ๋งŒ๋“ค์–ด์•ผ ํ•ด.

2. ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ > ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ
๊ธˆ์ง€ํ•  ๊ฒƒ์„ ๋‚˜์—ดํ•˜์ง€ ๋ง๊ณ , ํ—ˆ์šฉํ•  ๊ฒƒ๋งŒ ๋ช…์‹œํ•ด.

3. ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ ˆ๋Œ€ ๋ฏฟ์ง€ ๋งˆ
ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ณด๋‚ด๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋Š” ์˜์‹ฌํ•˜๊ณ  ๊ฒ€์ฆํ•ด์•ผ ํ•ด.

4. ์ตœ์†Œ ๊ถŒํ•œ ์›์น™
ํŒŒ์ผ ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ถŒํ•œ ๋“ฑ ๋ชจ๋“  ๊ฒƒ์„ ์ตœ์†Œํ•œ์œผ๋กœ ์„ค์ •ํ•ด.

5. ๋กœ๊น…๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง
๊ณต๊ฒฉ์„ ๋ง‰๋Š” ๊ฒƒ๋งŒํผ ๊ณต๊ฒฉ ์‹œ๋„๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ์ถ”์ ํ•˜๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•ด.

6. ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ๊ฐ์‚ฌ
์ตœ์†Œ ๋ถ„๊ธฐ๋งˆ๋‹ค ํ•œ ๋ฒˆ์”ฉ์€ ๋ณด์•ˆ ์ ๊ฒ€์„ ํ•ด์•ผ ํ•ด.

7. ์ตœ์‹  ๋ณด์•ˆ ํŠธ๋ Œ๋“œ ํŒ”๋กœ์šฐ
OWASP, CVE ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋“ฑ์„ ์ •๊ธฐ์ ์œผ๋กœ ํ™•์ธํ•ด.
โš ๏ธ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง์•„์•ผ ํ•  ๊ฒƒ๋“ค
  • โŒ ์—…๋กœ๋“œ๋œ ํŒŒ์ผ์„ ์›น ๋ฃจํŠธ์— ์ง์ ‘ ์ €์žฅ
  • โŒ ์›๋ณธ ํŒŒ์ผ๋ช…์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
  • โŒ ํ™•์žฅ์ž๋งŒ ๋ณด๊ณ  ํŒŒ์ผ ํ˜•์‹ ํŒ๋‹จ
  • โŒ MIME ํƒ€์ž…๋งŒ ๋ฏฟ๊ณ  ๊ฒ€์ฆ
  • โŒ ํŒŒ์ผ ํฌ๊ธฐ ์ œํ•œ ์—†์ด ์—…๋กœ๋“œ ํ—ˆ์šฉ
  • โŒ ์—…๋กœ๋“œ ์†๋„ ์ œํ•œ ์—†์ด ์šด์˜
  • โŒ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์— ์‹œ์Šคํ…œ ์ •๋ณด ๋…ธ์ถœ
  • โŒ ๋ณด์•ˆ ์—…๋ฐ์ดํŠธ ๋ฏธ๋ฃจ๊ธฐ

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„: ๋” ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด?

์ด ๊ธ€์„ ๋‹ค ์ฝ์—ˆ๋‹ค๋ฉด ๊ธฐ๋ณธ์€ ํ™•์‹คํžˆ ์žก์€ ๊ฑฐ์•ผ! ํ•˜์ง€๋งŒ ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ๊ฒƒ๋“ค์„ ์ถ”์ฒœํ•ด:
์ฃผ์ œ ์ถ”์ฒœ ๋ฆฌ์†Œ์Šค ๋‚œ์ด๋„
OWASP Top 10 https://owasp.org/www-project-top-ten/ ๐ŸŸข ์ดˆ๊ธ‰
์›น ๋ณด์•ˆ ๊ธฐ์ดˆ PortSwigger Web Security Academy ๐ŸŸข ์ดˆ๊ธ‰
ํŒŒ์ผ ์—…๋กœ๋“œ ์ทจ์•ฝ์  HackerOne Disclosed Reports ๐ŸŸก ์ค‘๊ธ‰
ํด๋ผ์šฐ๋“œ ๋ณด์•ˆ AWS Security Best Practices ๐ŸŸก ์ค‘๊ธ‰
์นจํˆฌ ํ…Œ์ŠคํŒ… OWASP Testing Guide ๐Ÿ”ด ๊ณ ๊ธ‰
๋ณด์•ˆ ์ธ์ฆ CEH, OSCP, CISSP ๐Ÿ”ด ๊ณ ๊ธ‰

๐Ÿ’ฌ ๋งˆ์ง€๋ง‰์œผ๋กœ...

๋ณด์•ˆ์€ ์–ด๋ ต๊ณ  ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ ์šฉํ•˜๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ์•ˆ์ „ํ•œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„๋ฒฝํ•  ํ•„์š”๋Š” ์—†์–ด. ์ค‘์š”ํ•œ ๊ฑด ๊ณ„์†ํ•ด์„œ ๊ฐœ์„ ํ•˜๋ ค๋Š” ์ž์„ธ์•ผ! ๐Ÿ’ช

ํ˜น์‹œ ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ ๊ด€๋ จํ•ด์„œ ๋„์›€์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜, ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋ฅผ ๋ฐ›๊ณ  ์‹ถ๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ๋ณด์•ˆ ์ „๋ฌธ๊ฐ€๋“ค์„ ์ฐพ์•„๋ณผ ์ˆ˜๋„ ์žˆ์–ด. ๋‹ค์–‘ํ•œ ๋ถ„์•ผ์˜ ์ „๋ฌธ๊ฐ€๋“ค์ด ๋ชจ์—ฌ ์žˆ์œผ๋‹ˆ๊นŒ ๋ง์ด์•ผ! ๐Ÿ˜Š

์ž, ์ด์ œ ๋‹น์‹ ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์•ˆ์ „ํ•˜๊ฒŒ ๋งŒ๋“ค ์ฐจ๋ก€์•ผ! ํ™”์ดํŒ…! ๐Ÿš€๐Ÿ›ก๏ธ
์•ˆ์ „ํ•œ ํŒŒ์ผ ์—…๋กœ๋“œ ๋‹น์‹ ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ง€ํ‚ค์„ธ์š” ๐Ÿ›ก๏ธ ๐Ÿ”’ โœ… ๋ณด์•ˆ์€ ์„ ํƒ์ด ์•„๋‹Œ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค

๐ŸŽฏ ์ด ๊ธ€์ด ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?

ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ์€ ๋ชจ๋“  ์›น ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ฐ˜๋“œ์‹œ ์•Œ์•„์•ผ ํ•  ํ•„์ˆ˜ ์ง€์‹์ด์—์š”.
์ด ๊ฐ€์ด๋“œ๋ฅผ ์ฐธ๊ณ ํ•ด์„œ ์•ˆ์ „ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”!
๋” ๋งŽ์€ ๊ฐœ๋ฐœ ์ง€์‹์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€๋“ค๊ณผ ์†Œํ†ตํ•ด๋ณด์„ธ์š”! ๐Ÿš€

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

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