Ver3.0 ๐ก๏ธ ํ์ผ ์ ๋ก๋ ๋ณด์ ์ทจ์ฝ์ ๊ณผ ์์ ํ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ: ๊ฐ๋ฐ์๊ฐ ๋ฐ๋์ ์์์ผ ํ ์๋ฒฝ ๊ฐ์ด๋

๐ก๏ธ ํ์ผ ์ ๋ก๋ ๋ณด์ ์ทจ์ฝ์ ๊ณผ ์์ ํ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ: ๊ฐ๋ฐ์๊ฐ ๋ฐ๋์ ์์์ผ ํ ์๋ฒฝ ๊ฐ์ด๋
์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ์ฅ ์ํํ ๊ณต๊ฒฉ ๋ฒกํฐ๋ฅผ ๋ง์๋ด๋ ์ค์ ๋ณด์ ์ ๋ต
์ด ๊ธ์์๋ ํ์ผ ์ ๋ก๋์ ๊ด๋ จ๋ ๋ชจ๋ ๋ณด์ ์ทจ์ฝ์ ๊ณผ ๊ทธ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ฏ ์ฝ๊ณ ์ฌ๋ฏธ์๊ฒ ํ์ด๋ณผ ๊ฑฐ์ผ. ์ค์ ์ฝ๋ ์์ ์ ํจ๊ป ๋ง์ด์ง! ๐
๐ฅ ํ์ผ ์ ๋ก๋, ์ ์ด๋ ๊ฒ ์ํํ ๊ฑธ๊น?
์, ์ด์ผ๊ธฐ๋ฅผ ํ๋ ํด์ค๊ฒ. ๋ด ์น๊ตฌ๊ฐ ์์ ์คํํธ์ ์์ ๊ฐ๋ฐ์๋ก ์ผํ๊ณ ์์์ด. ์ด๋ ๋ ํ๋กํ ์ฌ์ง ์ ๋ก๋ ๊ธฐ๋ฅ์ ๊ธํ๊ฒ ๋ง๋ค์ด์ผ ํ๋๋ฐ, ์๊ฐ์ ์ซ๊ฒจ์ ๊ธฐ๋ณธ์ ์ธ ๊ฒ์ฆ๋ง ํ๊ณ ๋ฐฐํฌํ๋. ๊ทธ๋ฐ๋ฐ ์ผ์ฃผ์ผ ๋ค์ ์๋ฒ๊ฐ ๋จนํต์ด ๋๋๋. ๐ฑ์๊ณ ๋ณด๋ ๋๊ตฐ๊ฐ๊ฐ PHP ์น์ ธ์ ์ด๋ฏธ์ง ํ์ผ๋ก ์์ฅํด์ ์ ๋ก๋ํ๊ณ , ๊ทธ๊ฑธ ํตํด ์๋ฒ ์ ์ฒด๋ฅผ ์ฅ์ ํ ๊ฑฐ์ผ. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๋ณด๋ ๋ฌผ๋ก ์ด๊ณ ๊ณ ๊ฐ ๊ฐ์ธ์ ๋ณด๊น์ง ์ ์ถ๋ ๋ปํ์ด. ๋คํํ ๋ฐฑ์ ์ด ์์ด์ ๋ณต๊ตฌํ์ง๋ง, ๊ทธ ์น๊ตฌ๋ ๋ฉฐ์น ๋์ ์ ๋ ๋ชป ์๊ณ ์ฌ๊ณ ์์ตํ๋๋ผ ๊ณ ์ํ๋.
์ด๊ฒ ๋ฐ๋ก ํ์ผ ์ ๋ก๋ ๋ณด์์ด ์ค์ํ ์ด์ ์ผ. ๋จ์ํด ๋ณด์ด๋ ๊ธฐ๋ฅ์ด์ง๋ง, ์๋ชป ๊ตฌํํ๋ฉด ์์คํ ์ ์ฒด๊ฐ ์ํ์ ๋ ธ์ถ๋ ์ ์๊ฑฐ๋ . ๐ฏ
โข ํ๊ท ํ์ง ์๊ฐ: 197์ผ (์ฝ 6๊ฐ์ ์ด์ ํด์ปค๊ฐ ์์คํ ๋ด๋ถ์ ์ ๋ณต)
โข ํ๊ท ํผํด ๋ณต๊ตฌ ๋น์ฉ: ์ฝ 4์ต 2์ฒ๋ง ์
โข ๊ฐ์ฅ ๋ง์ด ์ ๋ก๋๋๋ ์ ์ฑ ํ์ผ: ์น์ ธ (47%), ๋์ฌ์จ์ด (28%), ๋ฐฑ๋์ด (25%)
๐ญ ํ์ผ ์ ๋ก๋ ๊ณต๊ฒฉ์ ์ฃผ์ ์ ํ๋ค
ํ์ผ ์ ๋ก๋ ๊ณต๊ฒฉ์ ์ ๋ง ๋ค์ํ ํํ๋ก ๋ํ๋. ๋ง์น ๋ณ์ ์ ๊ท์ฌ ๊ฐ๋ฌ๊น? ํ๋์ฉ ์ดํด๋ณด์!1๏ธโฃ ์น์ ธ(Web Shell) ์ ๋ก๋ ๊ณต๊ฒฉ
ํด์ปค๊ฐ ์๋ฒ ์ธก ์คํฌ๋ฆฝํธ ํ์ผ(PHP, JSP, ASP ๋ฑ)์ ์ ๋ก๋ํด์ ์๋ฒ๋ฅผ ์๊ฒฉ์ผ๋ก ์ ์ดํ๋ ๊ณต๊ฒฉ์ด์ผ. ์ด๊ฒ ์ฑ๊ณตํ๋ฉด ํด์ปค๋ ์ฌ์ค์ ์๋ฒ์ ๊ด๋ฆฌ์๊ฐ ๋๋ ๊ฑฐ์ง.
<?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 ํ์
๋ง ๋ฏฟ๊ณ ๊ฒ์ฆํ๋ค๋ฉด? ๋น์ฐํ ๋ซ๋ฆฌ๊ฒ ์ง! ๐ช
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๋จ๊ณ: ํ์ดํธ๋ฆฌ์คํธ ๊ธฐ๋ฐ ํ์ฅ์ ๊ฒ์ฆ
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
});
});
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 ์ฝ๋ |
| 25 50 44 46 | "%PDF"์ ASCII ์ฝ๋ | |
| ZIP | 50 4B 03 04 | ZIP ์์นด์ด๋ธ |
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
});
});
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๋จ๊ณ: ์ด๋ฏธ์ง ํ์ผ ์ฌ์ฒ๋ฆฌ
์ด๋ฏธ์ง ํ์ผ์ ๊ฒฝ์ฐ, ํ ๋ฒ ๋ ์์ ์ฅ์น๋ฅผ ๋ ์ ์์ด. ๋ฐ๋ก ์ด๋ฏธ์ง๋ฅผ ๋ค์ ๊ทธ๋ฆฌ๋ ๊ฑฐ์ผ! ์ด๋ ๊ฒ ํ๋ฉด ์ด๋ฏธ์ง ์์ ์จ๊ฒจ์ง ์ ์ฑ ์ฝ๋๋ฅผ ์ ๊ฑฐํ ์ ์๊ฑฐ๋ . ๐จ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
});
});
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๋จ๊ณ: ์์ ํ ํ์ผ ์ ์ฅ ์์น
2. ํ์ผ ์ ๊ทผ์ ๋ณ๋์ ๋ค์ด๋ก๋ API๋ฅผ ํตํด์๋ง ๊ฐ๋ฅํ๊ฒ
3. ๋ค์ด๋ก๋ ์ ์ ์ ํ Content-Type ํค๋ ์ค์
4. Content-Disposition ํค๋๋ก ๋ค์ด๋ก๋ ๊ฐ์
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๋จ๊ณ: ์ถ๊ฐ ๋ณด์ ์กฐ์น๋ค
์ฌ๊ธฐ๊น์ง ํ์ผ๋ฉด ๊ธฐ๋ณธ์ ๋ค ํ ๊ฑฐ์ผ! ํ์ง๋ง ๋ ๊ฐ๋ ฅํ ๋ณด์์ ์ํ๋ค๋ฉด ์ด๋ฐ ๊ฒ๋ค๋ ๊ณ ๋ คํด๋ด:ClamAV ๊ฐ์ ์ํฐ๋ฐ์ด๋ฌ์ค ์์ง์ ํตํฉํด์ ์ ๋ก๋๋ ํ์ผ์ ์ค์๊ฐ์ผ๋ก ์ค์บํ ์ ์์ด. ํนํ ์ฌ์ฉ์๊ฐ ๋ง์ ์๋น์ค๋ผ๋ฉด ํ์!
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: 'ํ์ผ์ด ์์ ํ๊ฒ ์
๋ก๋๋์์ต๋๋ค.' });
});
๊ฐ์ IP์์ ์งง์ ์๊ฐ์ ๋๋ฌด ๋ง์ ํ์ผ์ ์ ๋ก๋ํ์ง ๋ชปํ๋๋ก ์ ํํด์ผ ํด. DoS ๊ณต๊ฒฉ์ ๋ง์ ์ ์๊ฑฐ๋ !
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) => {
// ... ์
๋ก๋ ์ฒ๋ฆฌ ...
});
์ค์ ํ์ผ๋ช ์ ์์ ํ ์จ๊ธฐ๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋งคํ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ๊ฑฐ์ผ. ์ด๋ ๊ฒ ํ๋ฉด ํด์ปค๊ฐ ํ์ผ ๊ตฌ์กฐ๋ฅผ ์ถ์ธกํ๊ธฐ ์ด๋ ค์์ ธ.
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}"`);
// ... ๋๋จธ์ง ํ์ผ ์ ์ก ๋ก์ง ...
});
AWS S3, Google Cloud Storage ๊ฐ์ ์ค๋ธ์ ํธ ์คํ ๋ฆฌ์ง๋ฅผ ์ฌ์ฉํ๋ฉด ๋ณด์์ด ๋ ๊ฐํ๋ผ. ์ด๋ฐ ์๋น์ค๋ค์ ์์ฒด์ ์ผ๋ก ๊ฐ๋ ฅํ ๋ณด์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๊ฑฐ๋ !
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 });
});
๐ ์ค์ ์ฒดํฌ๋ฆฌ์คํธ: ๋ด ์ฝ๋๋ ์์ ํ ๊น?
์, ์ด์ ์ค์ ์ด์ผ! ๋ด๊ฐ ๋ง๋ ํ์ผ ์ ๋ก๋ ๊ธฐ๋ฅ์ด ์์ ํ์ง ์ฒดํฌํด๋ณด์. ์ด ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ํ๋์ฉ ํ์ธํด๋ด! โ| ํญ๋ชฉ | ์ฒดํฌ ํฌ์ธํธ | ์ํ๋ |
|---|---|---|
| โ ํ์ฅ์ ๊ฒ์ฆ | ํ์ดํธ๋ฆฌ์คํธ ๋ฐฉ์์ผ๋ก ํ์ฉ๋ ํ์ฅ์๋ง ์ ๋ก๋ ๊ฐ๋ฅํ๊ฐ? | ๐ด Critical |
| โ ์ด์ค ํ์ฅ์ | file.php.jpg ๊ฐ์ ์ด์ค ํ์ฅ์๋ฅผ ์ฐจ๋จํ๋๊ฐ? | ๐ด Critical |
| โ ๋งค์ง ๋ฐ์ดํธ | ํ์ผ์ ์ค์ ๋ด์ฉ์ ๊ฒ์ฆํ๋๊ฐ? | ๐ด Critical |
| โ ํ์ผ ํฌ๊ธฐ | ์ต๋ ํ์ผ ํฌ๊ธฐ ์ ํ์ด ์๋๊ฐ? | ๐ก High |
| โ ํ์ผ๋ช ๊ฒ์ฆ | ๊ฒฝ๋ก ์กฐ์ ๋ฌธ์(../, ..\)๋ฅผ ์ฐจ๋จํ๋๊ฐ? | ๐ด Critical |
| โ ์ ์ฅ ์์น | ์น ๋ฃจํธ ๋ฐ์ ์์ ํ ๋๋ ํ ๋ฆฌ์ ์ ์ฅํ๋๊ฐ? | ๐ด Critical |
| โ ํ์ผ๋ช ๋ณ๊ฒฝ | ์๋ณธ ํ์ผ๋ช ์ ๊ทธ๋๋ก ์ฌ์ฉํ์ง ์๋๊ฐ? | ๐ก High |
| โ ์คํ ๊ถํ | ์ ๋ก๋ ๋๋ ํ ๋ฆฌ์ ์คํ ๊ถํ์ด ์๋๊ฐ? | ๐ด Critical |
| โ Content-Type | ๋ค์ด๋ก๋ ์ ์ฌ๋ฐ๋ฅธ Content-Type์ ์ค์ ํ๋๊ฐ? | ๐ก High |
| โ Rate Limiting | ์ ๋ก๋ ์๋ ์ ํ์ด ์๋๊ฐ? | ๐ก High |
| โ ๋ฐ์ด๋ฌ์ค ์ค์บ | ์ ๋ก๋๋ ํ์ผ์ ์ค์บํ๋๊ฐ? | ๐ข Medium |
| โ ๋ก๊น | ์ ๋ก๋ ํ๋์ ๋ก๊ทธ๋ก ๊ธฐ๋กํ๋๊ฐ? | ๐ข Medium |
๐ฌ ์ค์ ๊ณต๊ฒฉ ์๋๋ฆฌ์ค์ ๋ฐฉ์ด ์ ๋ต
์ด๋ก ๋ง ์๋ฉด ๋ญํด? ์ค์ ๋ก ์ด๋ป๊ฒ ๊ณต๊ฒฉ๋นํ๊ณ ์ด๋ป๊ฒ ๋ง๋์ง ์์์ผ์ง! ์ค์ ์๋๋ฆฌ์ค๋ฅผ ์ดํด๋ณด์. ๐ฌ์๋๋ฆฌ์ค 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๋ ์ ๋ง ๊ฐ๋ ฅํ ๋ณด์ ๊ธฐ๋ฅ๋ค์ ์ ๊ณตํด. ์ ๋๋ก๋ง ์ค์ ํ๋ฉด ๊ฑฐ์ ์๋ฒฝํ ๋ณด์์ ๊ตฌํํ ์ ์์ด!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 ์์ฑ์ ์คํจํ์ต๋๋ค.' });
}
}
);
โข โ ์๋ฒ ์ธก ์ํธํ(SSE) ํ์ฑํ
โข โ HTTPS๋ง ํ์ฉํ๋๋ก ๋ฒํท ์ ์ฑ ์ค์
โข โ Presigned URL๋ก๋ง ํ์ผ ์ ๊ทผ ํ์ฉ
โข โ IAM ์ญํ ๋ก ์ต์ ๊ถํ ์์น ์ ์ฉ
โข โ S3 ๋ฒํท ๋ฒ์ ๊ด๋ฆฌ ํ์ฑํ
โข โ CloudTrail๋ก ๋ชจ๋ ์ ๊ทผ ๋ก๊น
โข โ ์๋ช ์ฃผ๊ธฐ ์ ์ฑ ์ผ๋ก ์ค๋๋ ํ์ผ ์๋ ์ญ์
์ง์ ์ ๋ก๋(Direct Upload) ํจํด
์๋ฒ๋ฅผ ๊ฑฐ์น์ง ์๊ณ ํด๋ผ์ด์ธํธ์์ S3๋ก ์ง์ ์ ๋ก๋ํ๋ ๋ฐฉ๋ฒ๋ ์์ด. ์๋ฒ ๋ถํ๋ฅผ ์ค์ผ ์ ์๊ณ ๋ ๋น ๋ฅด์ง! ๐// ์๋ฒ: 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 ๋ค์ดํฐ๋ธ ์ฑ
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
}
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) ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์์๋ ๋ค์ํ ์ข ๋ฅ์ ํ์ผ์ด ์ ๋ก๋๋์์? ํฌํธํด๋ฆฌ์ค ์ด๋ฏธ์ง, ์์ ํ์ผ, ๊ณ์ฝ์ ๋ฑ๋ฑ... ๐จ๐์ฌ๋ฅ๋ท์์ ํ์ผ ์ ๋ก๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค๋ฉด ์ด๋ ๊ฒ ํ ์ ์์ด:
โข ์ผ๋ฐ ํ์: ์ด๋ฏธ์ง๋ง ์ ๋ก๋ ๊ฐ๋ฅ (ํ๋กํ, ํฌํธํด๋ฆฌ์ค)
โข ์ ๋ฌธ๊ฐ ํ์: ์ด๋ฏธ์ง + ๋ฌธ์ ํ์ผ (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();
}
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
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: 'ํต๊ณ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.' });
}
});
๐ ๋ง๋ฌด๋ฆฌ: ๋ณด์์ ์ฌ์ ์ด์ง ๋ชฉ์ ์ง๊ฐ ์๋์ผ
์, ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด! ๐ ํ์ผ ์ ๋ก๋ ๋ณด์์ ๋ํด ๊ฑฐ์ ๋ชจ๋ ๊ฑธ ๋ค๋ค๋ค๊ณ ๋ด๋ ๋ผ. ํ์ง๋ง ๊ธฐ์ตํด์ค, ๋ณด์์ ํ ๋ฒ ๊ตฌํํ๊ณ ๋๋๋ ๊ฒ ์๋์ผ. ๊ณ์ํด์ ์ ๋ฐ์ดํธํ๊ณ ๊ฐ์ ํด์ผ ํ๋ ๊ฑฐ์ง.ํ๋์ ๋ณด์ ์ฅ์น๋ง ๋ฏฟ์ง ๋ง. ์ฌ๋ฌ ๊ฒน์ ๋ณด์ ๊ณ์ธต์ ๋ง๋ค์ด์ผ ํด.
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
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.