Ver3.0 ๐ก๏ธ ์น ๋ณด์์ ํต์ฌ! OWASP Top 10 ์ทจ์ฝ์ ์๋ฒฝ ๋์ ๊ฐ์ด๋ ๐

๐ก๏ธ ์น ๋ณด์์ ํต์ฌ! OWASP Top 10 ์ทจ์ฝ์ ์๋ฒฝ ๋์ ๊ฐ์ด๋ ๐
์น ๊ฐ๋ฐ์๋ผ๋ฉด ๋ฐ๋์ ์์์ผ ํ ๋ณด์ ๊ฐํ ์ ๋ต
๐ฏ ์ ์น ๋ณด์์ด ์ด๋ ๊ฒ ์ค์ํด์ก์๊น?
์๋ , ์น๊ตฌ! ๐ ์ค๋์ ์น ๊ฐ๋ฐ์๋ผ๋ฉด ์ ๋ ํผํด๊ฐ ์ ์๋ ์ฃผ์ , ๋ฐ๋ก ์น ๋ณด์์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊น ํด. ํน์ ๋ด์ค์์ "๊ฐ์ธ์ ๋ณด ์ ์ถ ์ฌ๊ณ ", "ํดํน ํผํด" ๊ฐ์ ์์ ๋ค์ด๋ณธ ์ ์์ง? ๊ทธ๊ฒ ๋ฐ๋ก ์น ๋ณด์์ด ์ ๋๋ก ๋์ง ์์์ ๋ ์ผ์ด๋๋ ์ผ์ด์ผ. ๐ฑ
์์ฆ ์ธ์์ ์ ๋ง ๋ชจ๋ ๊ฒ ์จ๋ผ์ธ์ผ๋ก ์ฐ๊ฒฐ๋์ด ์์์? ์ผํ๋ ํ๊ณ , ์ํ ์ ๋ฌด๋ ๋ณด๊ณ , ์น๊ตฌ๋ค๊ณผ ์ํต๋ ํ๊ณ ... ์ด ๋ชจ๋ ๊ฒ ์น์ ํตํด ์ด๋ฃจ์ด์ง๋๋ฐ, ๋ง์ฝ ์ด ์น์ด ์์ ํ์ง ์๋ค๋ฉด? ์๊ฐ๋ง ํด๋ ๋์ฐํ์ง! ๐
โข ๋ฐ์ดํฐ ์ ์ถ ์ฌ๊ณ ์ ํ๊ท ๋น์ฉ์ ์ฝ 424๋ง ๋ฌ๋ฌ (์ฝ 50์ต ์!)
โข ์น ์ ํ๋ฆฌ์ผ์ด์ ๊ณต๊ฒฉ์ ์ ์ฒด ๋ณด์ ์ฌ๊ณ ์ 26%๋ฅผ ์ฐจ์ง
โข ๋ณด์ ์ทจ์ฝ์ ๋ฐ๊ฒฌ๋ถํฐ ํจ์น๊น์ง ํ๊ท 279์ผ ์์
์ด๋ฐ ๋ฌด์์ด ํต๊ณ๋ฅผ ๋ณด๋ฉด ์น ๋ณด์์ด ์ ์ค์ํ์ง ํ์คํ ์ ์ ์์ง? ๊ทธ๋์ ์ค๋์ ์น ๋ณด์์ ๋ฐ์ด๋ธ์ด๋ผ๊ณ ํ ์ ์๋ OWASP Top 10์ ์ค์ฌ์ผ๋ก ์ค์ ์์ ๋ฐ๋ก ์ ์ฉํ ์ ์๋ ๋ณด์ ๊ฐํ ๋ฐฉ์์ ์๋ ค์ค๊ฒ! ๐
๐ OWASP๊ฐ ๋ญ๊ธธ๋?
๋ณธ๊ฒฉ์ ์ผ๋ก ๋ค์ด๊ฐ๊ธฐ ์ ์, OWASP๊ฐ ๋ญ์ง๋ถํฐ ์์์ผ๊ฒ ์ง? OWASP๋ Open Web Application Security Project์ ์ฝ์๋ก, ์น ์ ํ๋ฆฌ์ผ์ด์ ๋ณด์์ ์ฐ๊ตฌํ๊ณ ๊ฐ์ ํ๋ ๊ตญ์ ์ ์ธ ๋น์๋ฆฌ ๋จ์ฒด์ผ. ๐
์ด ๋จ์ฒด์์๋ ๋งค๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ๊ฐ์ฅ ๋ง์ด ๋ฐ๊ฒฌ๋๊ณ ์ํํ ๋ณด์ ์ทจ์ฝ์ 10๊ฐ์ง๋ฅผ ์ ์ ํด์ ๋ฐํํ๋๋ฐ, ์ด๊ฒ ๋ฐ๋ก OWASP Top 10์ด์ผ! ๋ง์น ๋ณด์ ์ทจ์ฝ์ ์ "์ฐ๊ฐ ์ฐจํธ"๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ. ๐
์, ๊ทธ๋ผ ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก OWASP Top 10์ ๊ฐ ์ทจ์ฝ์ ๊ณผ ๊ทธ ๋์ ๋ฐฉ๋ฒ์ ํ๋์ฉ ํํค์ณ๋ณด์! ์ค๋น๋์ด? ๐
๐ฅ 1์: Broken Access Control (์ ๊ทผ ์ ์ด ์คํจ)
2021๋ OWASP Top 10์์ ๋น๋นํ 1์๋ฅผ ์ฐจ์งํ ์ทจ์ฝ์ ์ด์ผ! ์ด์ ์๋ 5์์๋๋ฐ 1์๋ก ๊ป์ถฉ ๋ฐ์ด์ฌ๋์ด. ๊ทธ๋งํผ ์ฌ๊ฐํ๋ค๋ ๋ป์ด์ง. ๐ฐ
๐ค ์ด๊ฒ ๋ญ๋ฐ?
๊ฐ๋จํ ๋งํ๋ฉด, ๊ถํ์ด ์๋ ์ฌ์ฉ์๊ฐ ๊ถํ์ด ํ์ํ ๊ธฐ๋ฅ์ด๋ ๋ฐ์ดํฐ์ ์ ๊ทผํ ์ ์๋ ์ทจ์ฝ์ ์ด์ผ. ์๋ฅผ ๋ค์ด๋ณผ๊น?
๐ฅ ์ค์ ๊ณต๊ฒฉ ์๋๋ฆฌ์ค
๊ณต๊ฒฉ์๊ฐ URL์ ์ง์ ์กฐ์ํด์ ๊ถํ ์๋ ํ์ด์ง์ ์ ๊ทผํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์. ์๋ฅผ ๋ค๋ฉด:
// ์ ์์ ์ธ ์ฌ์ฉ์ ํ์ด์ง
https://example.com/user/profile?id=123
// ๊ณต๊ฒฉ์๊ฐ URL์ ์กฐ์
https://example.com/user/profile?id=456 // ๋ค๋ฅธ ์ฌ๋์ ํ๋กํ!
https://example.com/admin/dashboard // ๊ด๋ฆฌ์ ํ์ด์ง!
์ด๋ ๊ฒ ๋จ์ํ URL๋ง ๋ฐ๊ฟ์ ๋ค๋ฅธ ์ฌ๋์ ์ ๋ณด๋ฅผ ๋ณผ ์ ์๋ค๋ฉด, ๊ทธ๊ฑด ์ฌ๊ฐํ ๋ณด์ ๋ฌธ์ ์ผ! ๐ฑ
๐ก๏ธ ๋์ ๋ฐฉ๋ฒ
๋ช ์์ ์ผ๋ก ํ์ฉ๋ ๊ฒ๋ง ์ ๊ทผ ๊ฐ๋ฅํ๋๋ก ์ค์ ํด์ผ ํด. ํ์ดํธ๋ฆฌ์คํธ ๋ฐฉ์์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ!
2. ์๋ฒ ์ฌ์ด๋์์ ๊ฒ์ฆํ๋ผ
ํด๋ผ์ด์ธํธ(๋ธ๋ผ์ฐ์ )์์์ ๊ฒ์ฆ์ ์ฝ๊ฒ ์ฐํ๋ ์ ์์ด. ๋ฐ๋์ ์๋ฒ์์ ๊ถํ์ ์ฒดํฌํด์ผ ํด!
3. ์ธ์ ๊ด๋ฆฌ๋ฅผ ์ฒ ์ ํ ํ๋ผ
๋ก๊ทธ์์ ์ ์ธ์ ์ ์์ ํ ๋ฌดํจํํ๊ณ , ์ธ์ ํ์์์์ ์ ์ ํ ์ค์ ํด์ผ ํด.
4. API ์ ๊ทผ ์ ์ด๋ฅผ ๊ฐํํ๋ผ
REST API๋ GraphQL ๊ฐ์ API ์๋ํฌ์ธํธ๋ ๋์ผํ๊ฒ ์ ๊ทผ ์ ์ด๋ฅผ ์ ์ฉํด์ผ ํด!
์ค์ ์ฝ๋๋ก ๋ณด๋ฉด ์ดํด๊ฐ ๋ ์ฌ์ธ ๊ฑฐ์ผ. ์๋ฅผ ๋ค์ด Node.js/Express์์๋ ์ด๋ ๊ฒ ๊ตฌํํ ์ ์์ด:
// ๋ฏธ๋ค์จ์ด๋ก ๊ถํ ์ฒดํฌ
function checkAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: '๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค' });
}
next();
}
function checkAdmin(req, res, next) {
if (!req.session.isAdmin) {
return res.status(403).json({ error: '๊ด๋ฆฌ์ ๊ถํ์ด ํ์ํฉ๋๋ค' });
}
next();
}
// ๋ผ์ฐํธ์ ์ ์ฉ
app.get('/user/profile', checkAuth, (req, res) => {
// ๋ณธ์ธ์ ํ๋กํ๋ง ์กฐํ ๊ฐ๋ฅํ๋๋ก
const requestedId = req.query.id;
const currentUserId = req.session.userId;
if (requestedId !== currentUserId) {
return res.status(403).json({ error: '๊ถํ์ด ์์ต๋๋ค' });
}
// ํ๋กํ ์ ๋ณด ๋ฐํ
});
app.get('/admin/dashboard', checkAuth, checkAdmin, (req, res) => {
// ๊ด๋ฆฌ์ ๋์๋ณด๋
});
๐ 2์: Cryptographic Failures (์ํธํ ์คํจ)
์ด์ ์๋ "Sensitive Data Exposure(๋ฏผ๊ฐํ ๋ฐ์ดํฐ ๋ ธ์ถ)"๋ผ๊ณ ๋ถ๋ ธ๋ ์ทจ์ฝ์ ์ด์ผ. ์ด๋ฆ์ด ๋ฐ๋ ์ด์ ๋ ๋จ์ํ ๋ฐ์ดํฐ๊ฐ ๋ ธ์ถ๋๋ ๊ฒ ๋ฌธ์ ๊ฐ ์๋๋ผ, ์ํธํ๊ฐ ์ ๋๋ก ๋์ง ์์์ ๋ฌธ์ ๊ฐ ์๊ธด๋ค๋ ๊ฑธ ๊ฐ์กฐํ๊ธฐ ์ํด์์ผ. ๐
๐ค ์ด๋ค ๋ฌธ์ ๊ฐ ์๊ธธ๊น?
๋ฏผ๊ฐํ ์ ๋ณด(๋น๋ฐ๋ฒํธ, ์ ์ฉ์นด๋ ๋ฒํธ, ๊ฐ์ธ์ ๋ณด ๋ฑ)๊ฐ ์ํธํ๋์ง ์๊ฑฐ๋, ์ฝํ ์ํธํ ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํ๊ฑฐ๋, ์ํธํ ํค ๊ด๋ฆฌ๊ฐ ์๋ชป๋๋ฉด ๋ฐ์ํ๋ ๋ฌธ์ ์ผ. ๐จ
๐ก๏ธ ๋์ ๋ฐฉ๋ฒ
์ํธํ๋ ์๊ฐ๋ณด๋ค ๋ณต์กํ๊ณ ์ฌ์ธํ ์์ ์ด์ผ. ํ๋์ฉ ์ดํด๋ณด์! ๐
โข HTTPS๋ฅผ ํ์๋ก ์ฌ์ฉ (TLS 1.2 ์ด์)
โข HTTP์์ HTTPS๋ก ์๋ ๋ฆฌ๋ค์ด๋ ํธ ์ค์
โข HSTS(HTTP Strict Transport Security) ํค๋ ์ฌ์ฉ
2. ์ ์ฅ๋ ๋ฐ์ดํฐ ๋ณดํธ (Data at Rest)
โข ๋น๋ฐ๋ฒํธ๋ ์ ๋ ํ๋ฌธ์ผ๋ก ์ ์ฅํ์ง ๋ง ๊ฒ!
โข bcrypt, Argon2, PBKDF2 ๊ฐ์ ๊ฐ๋ ฅํ ํด์ ํจ์ ์ฌ์ฉ
โข ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ AES-256 ๊ฐ์ ๊ฐ๋ ฅํ ์ํธํ ์๊ณ ๋ฆฌ์ฆ ์ฌ์ฉ
3. ์ํธํ ํค ๊ด๋ฆฌ
โข ์ํธํ ํค๋ฅผ ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ๋ง ๊ฒ!
โข ํ๊ฒฝ ๋ณ์๋ ์ ์ฉ ํค ๊ด๋ฆฌ ์๋น์ค(AWS KMS, Azure Key Vault ๋ฑ) ์ฌ์ฉ
โข ์ ๊ธฐ์ ์ผ๋ก ํค ๋กํ ์ด์ (๊ต์ฒด) ์ํ
์ค์ ์ฝ๋ ์์๋ฅผ ๋ณผ๊น? ๋น๋ฐ๋ฒํธ ํด์ฑ์ ์ ๋๋ก ํ๋ ๋ฐฉ๋ฒ์ด์ผ:
// โ ์ ๋ ์ด๋ ๊ฒ ํ์ง ๋ง!
const password = 'user123';
const hashedPassword = md5(password); // MD5๋ ๋๋ฌด ์ฝํด!
// โ ์ด๊ฒ๋ ์ ๋ผ!
const hashedPassword = sha1(password); // SHA1๋ ์ด์ ์์ ํ์ง ์์!
// โ
์ด๋ ๊ฒ ํด์ผ ํด! (bcrypt ์ฌ์ฉ)
const bcrypt = require('bcrypt');
const saltRounds = 12; // ์ถฉ๋ถํ ๋์ ๊ฐ ์ฌ์ฉ
// ํ์๊ฐ์
์
async function registerUser(password) {
const hashedPassword = await bcrypt.hash(password, saltRounds);
// DB์ hashedPassword ์ ์ฅ
return hashedPassword;
}
// ๋ก๊ทธ์ธ ์
async function loginUser(inputPassword, storedHash) {
const isValid = await bcrypt.compare(inputPassword, storedHash);
return isValid;
}
๊ทธ๋ฆฌ๊ณ HTTPS ์ค์ ๋ ์ค์ํด! Node.js/Express์์๋ ์ด๋ ๊ฒ ํ ์ ์์ด:
const express = require('express');
const helmet = require('helmet');
const app = express();
// Helmet์ผ๋ก ๋ณด์ ํค๋ ์ค์
app.use(helmet());
// HSTS ๊ฐ์ (๋ชจ๋ ์ฐ๊ฒฐ์ HTTPS๋ก)
app.use(helmet.hsts({
maxAge: 31536000, // 1๋
includeSubDomains: true,
preload: true
}));
// HTTP๋ฅผ HTTPS๋ก ๋ฆฌ๋ค์ด๋ ํธ
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
๐ 3์: Injection (์ธ์ ์ )
๋๋์ด ๋์๋ค! ์น ๋ณด์ ํ๋ฉด ๊ฐ์ฅ ๋จผ์ ๋ ์ค๋ฅด๋ ๊ทธ ์ ๋ช ํ SQL Injection! ์ด์ ๋ฒ์ ์์๋ 1์์๋๋ฐ, 3์๋ก ๋ด๋ ค์์ด. ํ์ง๋ง ์ฌ์ ํ ๋งค์ฐ ์ํํ ์ทจ์ฝ์ ์ด์ผ. ๐
๐ค ์ธ์ ์ ์ด ๋ญ์ผ?
์ฌ์ฉ์ ์ ๋ ฅ์ ์ ๋๋ก ๊ฒ์ฆํ์ง ์์์, ๊ณต๊ฒฉ์๊ฐ ์ ์์ ์ธ ์ฝ๋๋ฅผ "์ฃผ์ (injection)"ํ ์ ์๋ ์ทจ์ฝ์ ์ด์ผ. SQL ์ธ์ ์ ์ด ๊ฐ์ฅ ์ ๋ช ํ์ง๋ง, ๋ค๋ฅธ ์ข ๋ฅ๋ ๋ง์:
| ์ธ์ ์ ์ข ๋ฅ | ์ค๋ช | ์ํ๋ |
|---|---|---|
| SQL Injection | ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ์ ์ ์์ ์ธ SQL ์ฝ๋ ์ฝ์ | ๐ด ๋งค์ฐ ๋์ |
| NoSQL Injection | MongoDB ๋ฑ NoSQL DB์ ์ ์์ ์ธ ์ฟผ๋ฆฌ ์ฝ์ | ๐ด ๋งค์ฐ ๋์ |
| OS Command Injection | ์์คํ ๋ช ๋ น์ด ์คํ | ๐ด ๋งค์ฐ ๋์ |
| LDAP Injection | LDAP ์ฟผ๋ฆฌ ์กฐ์ | ๐ก ๋์ |
| XPath Injection | XML ๋ฐ์ดํฐ ์ฟผ๋ฆฌ ์กฐ์ | ๐ก ๋์ |
๐ฅ SQL Injection ๊ณต๊ฒฉ ์์
๊ฐ์ฅ ํด๋์ํ SQL Injection ๊ณต๊ฒฉ์ ๋ณด์ฌ์ค๊ฒ:
// โ ์ทจ์ฝํ ์ฝ๋
const username = req.body.username;
const password = req.body.password;
const query = `SELECT * FROM users WHERE username='${username}' AND password='${password}'`;
db.query(query);
// ๊ณต๊ฒฉ์๊ฐ ์ด๋ ๊ฒ ์
๋ ฅํ๋ฉด?
username: admin' --
password: (์๋ฌด๊ฑฐ๋)
// ์ค์ ์คํ๋๋ ์ฟผ๋ฆฌ:
SELECT * FROM users WHERE username='admin' --' AND password='...'
// -- ๋ค๋ ์ฃผ์ ์ฒ๋ฆฌ๋์ด ๋น๋ฐ๋ฒํธ ์ฒดํฌ๊ฐ ๋ฌด์๋จ!
์ด๋ ๊ฒ ๋๋ฉด ๊ณต๊ฒฉ์๊ฐ ๋น๋ฐ๋ฒํธ ์์ด๋ ๊ด๋ฆฌ์ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํ ์ ์์ด! ๐ฑ ๋ ๋ฌด์์ด ๊ฑด, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฒด๋ฅผ ์ญ์ ํ ์๋ ์๋ค๋ ๊ฑฐ์ผ:
// ๊ณต๊ฒฉ์ ์
๋ ฅ
username: admin'; DROP TABLE users; --
// ์คํ๋๋ ์ฟผ๋ฆฌ:
SELECT * FROM users WHERE username='admin'; DROP TABLE users; --' AND password='...'
// users ํ
์ด๋ธ์ด ์ญ์ ๋จ! ๐ฃ
๐ก๏ธ ๋์ ๋ฐฉ๋ฒ
๊ฐ์ฅ ํจ๊ณผ์ ์ธ ๋ฐฉ์ด ๋ฐฉ๋ฒ! SQL ์ฟผ๋ฆฌ์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฆฌํด์ ์ฒ๋ฆฌํด.
2. ORM(Object-Relational Mapping) ์ฌ์ฉ
Sequelize, TypeORM, Prisma ๊ฐ์ ORM์ ์ฌ์ฉํ๋ฉด ์๋์ผ๋ก ์ธ์ ์ ์ ๋ฐฉ์ดํด์ค.
3. ์ ๋ ฅ ๊ฒ์ฆ ๋ฐ ํ์ดํธ๋ฆฌ์คํธ
ํ์ฉ๋ ๋ฌธ์๋ง ์ ๋ ฅ๋ฐ๋๋ก ์ ํํด.
4. ์ต์ ๊ถํ ์์น
๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ณ์ ์ ํ์ํ ์ต์ํ์ ๊ถํ๋ง ๋ถ์ฌํด.
์ฌ๋ฐ๋ฅธ ์ฝ๋ ์์๋ฅผ ๋ณด์:
// โ
Prepared Statement ์ฌ์ฉ (MySQL)
const mysql = require('mysql2/promise');
async function loginUser(username, password) {
const connection = await mysql.createConnection({...});
// ? ํ๋ ์ด์คํ๋ ์ฌ์ฉ - ์๋์ผ๋ก ์ด์ค์ผ์ดํ ์ฒ๋ฆฌ๋จ
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = ? AND password = ?',
[username, password]
);
return rows[0];
}
// โ
ORM ์ฌ์ฉ (Sequelize)
const { User } = require('./models');
async function loginUser(username, password) {
const user = await User.findOne({
where: {
username: username,
password: password // ์ค์ ๋ก๋ ํด์๋ ๋น๋ฐ๋ฒํธ์ ๋น๊ตํด์ผ ํจ!
}
});
return user;
}
// โ
NoSQL Injection ๋ฐฉ์ด (MongoDB)
const sanitize = require('mongo-sanitize');
async function findUser(username) {
// ์
๋ ฅ๊ฐ ์ ์
const cleanUsername = sanitize(username);
const user = await User.findOne({ username: cleanUsername });
return user;
}
OS Command Injection๋ ์กฐ์ฌํด์ผ ํด:
// โ ์ํํ ์ฝ๋
const { exec } = require('child_process');
const filename = req.query.file;
exec(`cat ${filename}`, (error, stdout) => {
res.send(stdout);
});
// ๊ณต๊ฒฉ์๊ฐ ์ด๋ ๊ฒ ์
๋ ฅํ๋ฉด?
file: test.txt; rm -rf /
// ์์คํ
์ ์ฒด๊ฐ ์ญ์ ๋ ์ ์์ด! ๐ฑ
// โ
์์ ํ ์ฝ๋
const { execFile } = require('child_process');
const path = require('path');
// ํ์ดํธ๋ฆฌ์คํธ ๋ฐฉ์์ผ๋ก ๊ฒ์ฆ
const allowedFiles = ['file1.txt', 'file2.txt', 'file3.txt'];
const filename = req.query.file;
if (!allowedFiles.includes(filename)) {
return res.status(400).send('Invalid file');
}
// execFile์ ์์ ๊ฑฐ์น์ง ์์ ๋ ์์
execFile('cat', [path.join('/safe/directory/', filename)], (error, stdout) => {
res.send(stdout);
});
๐จ 4์: Insecure Design (์์ ํ์ง ์์ ์ค๊ณ)
2021๋ ์ ์๋กญ๊ฒ ์ถ๊ฐ๋ ์นดํ ๊ณ ๋ฆฌ์ผ! ์ด๊ฑด ์ฝ๋์ ๋ฒ๊ทธ๊ฐ ์๋๋ผ, ์ค๊ณ ๋จ๊ณ๋ถํฐ ๋ณด์์ ๊ณ ๋ คํ์ง ์์์ ์๊ธฐ๋ ๋ฌธ์ ๋ฅผ ๋ค๋ค. ๐๏ธ
๐ค ์์ ํ์ง ์์ ์ค๊ณ๋?
์๋ฌด๋ฆฌ ์ฝ๋๋ฅผ ์๋ฒฝํ๊ฒ ์์ฑํด๋, ์ค๊ณ ์์ฒด๊ฐ ์๋ชป๋๋ฉด ๋ณด์ ๋ฌธ์ ๊ฐ ์๊ธธ ์๋ฐ์ ์์ด. ์๋ฅผ ๋ค์ด๋ณผ๊น?
๐ฅ ํํ ์ค๊ณ ์ค์๋ค
โข ์ฟ ํฐ์ ๋ฌด์ ํ์ผ๋ก ์ฌ์ฉํ ์ ์๋ ์์คํ
โข ๊ฐ๊ฒฉ์ ํด๋ผ์ด์ธํธ์์ ์ ์ก๋ฐ๋ ๊ฒฐ์ ์์คํ
โข ๊ณ์ข ์ด์ฒด ์ ์์ก ํ์ธ์ ์ ๋๋ก ํ์ง ์๋ ์์คํ
2. ์ธ์ฆ/์ธ๊ฐ ๋ก์ง ๋ถ์ฌ
โข "๋์ค์ ์ถ๊ฐํ์"๋ฉฐ ์ธ์ฆ์ ๊ฑด๋๋ด API
โข ๋ชจ๋ ์ฌ์ฉ์์๊ฒ ๋์ผํ ๊ถํ์ ๋ถ์ฌํ๋ ์์คํ
3. ๋ณด์ ์๊ตฌ์ฌํญ ๋๋ฝ
โข ๊ฐ์ธ์ ๋ณด ์ํธํ ์๊ตฌ์ฌํญ์ด ์๋ ์ค๊ณ
โข ๋ก๊ทธ ๊ธฐ๋ก์ด ์์ด ๊ณต๊ฒฉ์ ์ถ์ ํ ์ ์๋ ์์คํ
โข ๋ฐฑ์ ๋ฐ ๋ณต๊ตฌ ๊ณํ์ด ์๋ ์์คํ
๐ก๏ธ ์์ ํ ์ค๊ณ ์์น
๋ณด์์ ๊ณ ๋ คํ ์ค๊ณ๋ฅผ ํ๋ ค๋ฉด ์ด๋ฐ ์์น๋ค์ ๋ฐ๋ผ์ผ ํด:
์ค๊ณ ๋จ๊ณ์์ "์ด๋ค ๊ณต๊ฒฉ์ด ๊ฐ๋ฅํ ๊น?"๋ฅผ ๋ฏธ๋ฆฌ ์๊ฐํด๋ด์ผ ํด. STRIDE ๋ชจ๋ธ์ด ์ ๋ช ํด:
โข Spoofing (์ ์ ์์กฐ)
โข Tampering (๋ฐ์ดํฐ ๋ณ์กฐ)
โข Repudiation (๋ถ์ธ)
โข Information Disclosure (์ ๋ณด ๋ ธ์ถ)
โข Denial of Service (์๋น์ค ๊ฑฐ๋ถ)
โข Elevation of Privilege (๊ถํ ์์น)
2. ์ต์ ๊ถํ ์์น (Principle of Least Privilege)
๊ฐ ์ปดํฌ๋ํธ์ ์ฌ์ฉ์์๊ฒ ํ์ํ ์ต์ํ์ ๊ถํ๋ง ๋ถ์ฌํด.
3. ์ฌ์ธต ๋ฐฉ์ด (Defense in Depth)
์ฌ๋ฌ ๊ณ์ธต์ ๋ณด์ ์ฅ์น๋ฅผ ๋์ด, ํ ๊ณ์ธต์ด ๋ซ๋ ค๋ ๋ค๋ฅธ ๊ณ์ธต์์ ๋ง์ ์ ์๋๋ก ํด.
4. ๊ธฐ๋ณธ์ ์ผ๋ก ์์ (Secure by Default)
์ฌ์ฉ์๊ฐ ๋ณ๋ ์ค์ ์ ํ์ง ์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์์ ํ ์ํ์ฌ์ผ ํด.
5. ์คํจ ์ ์์ (Fail Securely)
์๋ฌ๊ฐ ๋ฐ์ํด๋ ๋ณด์์ด ์ ์ง๋์ด์ผ ํด. ์๋ฅผ ๋ค์ด, ์ธ์ฆ ์คํจ ์ ์ ๊ทผ์ ํ์ฉํ๋ฉด ์ ๋ผ!
์ค์ ์์๋ฅผ ๋ณด์. ์ ์์๊ฑฐ๋ ์ฌ์ดํธ์ ์ฟ ํฐ ์์คํ ์ ์ค๊ณํ๋ค๊ณ ๊ฐ์ ํด๋ณผ๊ฒ:
// โ ์์ ํ์ง ์์ ์ค๊ณ
// ํด๋ผ์ด์ธํธ์์ ์ฟ ํฐ ํ ์ธ์ ๊ณ์ฐ
function applyCoupon(originalPrice, couponCode) {
let discount = 0;
if (couponCode === 'SAVE10') {
discount = originalPrice * 0.1;
}
const finalPrice = originalPrice - discount;
// ์๋ฒ๋ก ์ ์ก
fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({
price: finalPrice, // ์กฐ์ ๊ฐ๋ฅ!
coupon: couponCode
})
});
}
// โ
์์ ํ ์ค๊ณ
// ์๋ฒ์์ ๋ชจ๋ ๊ณ์ฐ๊ณผ ๊ฒ์ฆ ์ํ
// ํด๋ผ์ด์ธํธ
function applyCoupon(productId, couponCode) {
fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({
productId: productId, // ์ ํ ID๋ง ์ ์ก
coupon: couponCode
})
});
}
// ์๋ฒ
async function processCheckout(productId, couponCode, userId) {
// 1. ์ ํ ๊ฐ๊ฒฉ ์กฐํ (DB์์)
const product = await Product.findById(productId);
let finalPrice = product.price;
// 2. ์ฟ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ
const coupon = await Coupon.findOne({ code: couponCode });
if (coupon) {
// ์ฟ ํฐ ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ ์ฒดํฌ
if (coupon.expiryDate < new Date()) {
throw new Error('์ฟ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค');
}
// ์ฌ์ฉ์๊ฐ ์ด๋ฏธ ์ฌ์ฉํ๋์ง ์ฒดํฌ
const alreadyUsed = await CouponUsage.findOne({
userId: userId,
couponId: coupon.id
});
if (alreadyUsed) {
throw new Error('์ด๋ฏธ ์ฌ์ฉํ ์ฟ ํฐ์
๋๋ค');
}
// ํ ์ธ ์ ์ฉ
finalPrice = finalPrice * (1 - coupon.discountRate);
// ์ฟ ํฐ ์ฌ์ฉ ๊ธฐ๋ก
await CouponUsage.create({
userId: userId,
couponId: coupon.id,
usedAt: new Date()
});
}
// 3. ๊ฒฐ์ ์ฒ๋ฆฌ
return processPayment(userId, finalPrice);
}
โ ์ด ๊ธฐ๋ฅ์ด ์ ์ฉ๋ ์ ์๋ ๋ฐฉ๋ฒ์?
โ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃจ๋? ์ด๋ป๊ฒ ๋ณดํธํ ๊ฒ์ธ๊ฐ?
โ ์ธ์ฆ๊ณผ ๊ถํ ์ฒดํฌ๊ฐ ํ์ํ๊ฐ?
โ ์ ๋ ฅ ๊ฒ์ฆ์ ์ถฉ๋ถํ๊ฐ?
โ ์๋ฌ ๋ฐ์ ์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ๊ฒ์ธ๊ฐ?
โ ๋ก๊ทธ๋ ์ถฉ๋ถํ ๋จ๊ธฐ๊ณ ์๋?
โ ์ฑ๋ฅ ๊ณต๊ฒฉ(DoS)์ ์ทจ์ฝํ์ง ์์๊ฐ?
โ๏ธ 5์: Security Misconfiguration (๋ณด์ ๊ตฌ์ฑ ์ค๋ฅ)
์ด๊ฑด ์ ๋ง ํํ๊ฒ ๋ฐ์ํ๋ ๋ฌธ์ ์ผ! ์๋ฌด๋ฆฌ ์ข์ ๋ณด์ ๊ธฐ๋ฅ์ด ์์ด๋, ์ค์ ์ ์๋ชปํ๋ฉด ๋ฌด์ฉ์ง๋ฌผ์ด ๋๊ฑฐ๋ . ๐
๐ค ์ด๋ค ์ค์ ์ค๋ฅ๊ฐ ์์๊น?
โข ๊ธฐ๋ณธ ๊ด๋ฆฌ์ ๊ณ์ (admin/admin) ๊ทธ๋๋ก ์ฌ์ฉ
โข ๊ธฐ๋ณธ ํฌํธ ๋ฒํธ ๊ทธ๋๋ก ์ฌ์ฉ
โข ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ด๋ ํ์ด์ง๋ฅผ ์ญ์ ํ์ง ์์
2. ๋ถํ์ํ ๊ธฐ๋ฅ ํ์ฑํ
โข ๋๋ฒ๊ทธ ๋ชจ๋๋ฅผ ํ๋ก๋์ ์์๋ ์ผ๋์
โข ์ฌ์ฉํ์ง ์๋ HTTP ๋ฉ์๋(TRACE, OPTIONS ๋ฑ) ํ์ฉ
โข ๋๋ ํ ๋ฆฌ ๋ฆฌ์คํ ํ์ฑํ
3. ์๋ฌ ๋ฉ์์ง ๋ ธ์ถ
โข ์คํ ํธ๋ ์ด์ค๋ฅผ ์ฌ์ฉ์์๊ฒ ๊ทธ๋๋ก ๋ณด์ฌ์ค
โข ๋ฐ์ดํฐ๋ฒ ์ด์ค ์๋ฌ ๋ฉ์์ง ๋ ธ์ถ
โข ์์คํ ๊ฒฝ๋ก ์ ๋ณด ๋ ธ์ถ
4. ๋ณด์ ํค๋ ๋๋ฝ
โข CORS ์ค์ ์ค๋ฅ
โข CSP(Content Security Policy) ๋ฏธ์ค์
โข X-Frame-Options ๋ฏธ์ค์
๐ฅ ์ค์ ์ฌ๋ก
๐ก๏ธ ์ฌ๋ฐ๋ฅธ ๋ณด์ ๊ตฌ์ฑ
Node.js/Express ์ ํ๋ฆฌ์ผ์ด์ ์ ์๋ก ๋ค์ด๋ณผ๊ฒ. ํ๋ก๋์ ํ๊ฒฝ์์ ํ์์ ์ธ ๋ณด์ ์ค์ ๋ค์ด์ผ:
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const mongoSanitize = require('express-mongo-sanitize');
const hpp = require('hpp');
const app = express();
// 1. Helmet์ผ๋ก ๋ณด์ ํค๋ ์ค์
app.use(helmet());
// 2. CSP (Content Security Policy) ์ค์
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
}));
// 3. Rate Limiting (DDoS ๋ฐฉ์ด)
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // ์ต๋ 100๊ฐ ์์ฒญ
message: '๋๋ฌด ๋ง์ ์์ฒญ์ ๋ณด๋์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.'
});
app.use('/api/', limiter);
// 4. NoSQL Injection ๋ฐฉ์ด
app.use(mongoSanitize());
// 5. HTTP Parameter Pollution ๋ฐฉ์ด
app.use(hpp());
// 6. CORS ์ค์
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://yourdomain.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
// 7. ์๋ฌ ํธ๋ค๋ง (ํ๋ก๋์
)
if (process.env.NODE_ENV === 'production') {
app.use((err, req, res, next) => {
console.error(err.stack); // ์๋ฒ ๋ก๊ทธ์๋ง ๊ธฐ๋ก
// ์ฌ์ฉ์์๊ฒ๋ ์ผ๋ฐ์ ์ธ ๋ฉ์์ง๋ง ์ ์ก
res.status(500).json({
error: '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.'
// ์คํ ํธ๋ ์ด์ค๋ ์์ธ ์ ๋ณด๋ ๋
ธ์ถํ์ง ์์!
});
});
} else {
// ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง ์์ธ ์๋ฌ ํ์
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message,
stack: err.stack
});
});
}
// 8. ๋ถํ์ํ ํค๋ ์ ๊ฑฐ
app.disable('x-powered-by');
// 9. HTTPS ๊ฐ์
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https' && process.env.NODE_ENV === 'production') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ ๋ ์ค์ํด! MongoDB ์์๋ฅผ ๋ณด์:
// MongoDB ๋ณด์ ์ค์
// 1. ์ธ์ฆ ํ์ฑํ
// mongod.conf ํ์ผ์์:
security:
authorization: enabled
// 2. ๊ด๋ฆฌ์ ๊ณ์ ์์ฑ
use admin
db.createUser({
user: "adminUser",
pwd: "๊ฐ๋ ฅํ๋น๋ฐ๋ฒํธ123!@#", // ์ ๋ ์ฝํ ๋น๋ฐ๋ฒํธ ์ฌ์ฉ ๊ธ์ง!
roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
})
// 3. ์ ํ๋ฆฌ์ผ์ด์
์ฉ ๊ณ์ ์์ฑ (์ต์ ๊ถํ)
use myapp
db.createUser({
user: "appUser",
pwd: "๋๋ค๋ฅธ๊ฐ๋ ฅํ๋น๋ฐ๋ฒํธ456!@#",
roles: [ { role: "readWrite", db: "myapp" } ]
})
// 4. ์ธ๋ถ ์ ๊ทผ ์ ํ
// mongod.conf ํ์ผ์์:
net:
bindIp: 127.0.0.1 // ๋ก์ปฌํธ์คํธ๋ง ํ์ฉ
# ๋๋ ํน์ IP๋ง ํ์ฉ
# bindIp: 127.0.0.1,192.168.1.100
// 5. TLS/SSL ํ์ฑํ
net:
ssl:
mode: requireSSL
PEMKeyFile: /path/to/mongodb.pem
๋ฏผ๊ฐํ ์ค์ ์ ๋ณด(API ํค, ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋น๋ฐ๋ฒํธ ๋ฑ)๋ ์ ๋ ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ๋ง! .env ํ์ผ์ ์ฌ์ฉํ๊ณ , ์ด ํ์ผ์ .gitignore์ ์ถ๊ฐํด์ Git์ ์ฌ๋ผ๊ฐ์ง ์๋๋ก ํด์ผ ํด.
# .env ํ์ผ
NODE_ENV=production
DB_HOST=localhost
DB_USER=appUser
DB_PASSWORD=๊ฐ๋ ฅํ๋น๋ฐ๋ฒํธ456!@#
JWT_SECRET=๋งค์ฐ๊ธด๋๋ค๋ฌธ์์ด
API_KEY=your_api_key_here
# .gitignore ํ์ผ
.env
.env.local
.env.production
AWS, Azure, GCP ๊ฐ์ ํด๋ผ์ฐ๋ ํ๊ฒฝ์์๋ ์ ์ฉ ๋น๋ฐ ๊ด๋ฆฌ ์๋น์ค๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ๋ ์์ ํด:
โข Azure Key Vault: Azure์ ํค ๋ฐ ๋น๋ฐ ๊ด๋ฆฌ ์๋น์ค
โข Google Secret Manager: GCP์ ๋น๋ฐ ๊ด๋ฆฌ ์๋น์ค
โข HashiCorp Vault: ๋ฉํฐ ํด๋ผ์ฐ๋ ์ง์ ๋น๋ฐ ๊ด๋ฆฌ ๋๊ตฌ
๐ 6์: Vulnerable and Outdated Components (์ทจ์ฝํ๊ณ ์ค๋๋ ๊ตฌ์ฑ์์)
์์ฆ ์น ๊ฐ๋ฐ์ ์๋ง์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ์์? npm, pip, Maven... ์ด๋ฐ ํจํค์ง ๊ด๋ฆฌ์๋ก ์ฝ๊ฒ ์ค์นํ ์ ์์ง๋ง, ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ์ทจ์ฝ์ ์ ๊ฐ์ง๊ณ ์๋ค๋ฉด? ํฐ ๋ฌธ์ ๊ฐ ๋ ์ ์์ด! ๐ฐ
๐ค ์ ์ํํ ๊น?
โข ์ด ์ค ์ฝ 80%๊ฐ ์ต์ ํ๋ ์ด์์ ์๋ ค์ง ์ทจ์ฝ์ ์ ํฌํจ
โข ์ทจ์ฝ์ ์ด ๊ณต๊ฐ๋ ํ ํจ์น๊น์ง ํ๊ท 69์ผ ์์
โข ํ์ง๋ง ๊ณต๊ฒฉ์๋ค์ ์ทจ์ฝ์ ๊ณต๊ฐ ํ ๋ช ์๊ฐ ๋ด์ ๊ณต๊ฒฉ ์์!
๐ฅ ์ค์ ์ฌ๋ก๋ค
๐ก๏ธ ๋์ ๋ฐฉ๋ฒ
โข ์ฌ์ฉ ์ค์ธ ๋ชจ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ฒ์ ์ ๋ฌธ์ํ
โข ์ง์ ์์กด์ฑ๋ฟ๋ง ์๋๋ผ ๊ฐ์ ์์กด์ฑ(์์กด์ฑ์ ์์กด์ฑ)๋ ํ์
2. ์ ๊ธฐ์ ์ธ ์ ๋ฐ์ดํธ
โข ๋ณด์ ํจ์น๋ ์ต์ฐ์ ์ผ๋ก ์ ์ฉ
โข ๋ฉ์ด์ ๋ฒ์ ์ ๊ทธ๋ ์ด๋๋ ํ ์คํธ ํ ์ ์ฉ
3. ์๋ํ๋ ์ทจ์ฝ์ ์ค์บ
โข CI/CD ํ์ดํ๋ผ์ธ์ ๋ณด์ ์ค์บ ํตํฉ
โข ์ ๊ธฐ์ ์ผ๋ก ์์กด์ฑ ์ทจ์ฝ์ ์ฒดํฌ
4. ๋ถํ์ํ ์์กด์ฑ ์ ๊ฑฐ
โข ์ฌ์ฉํ์ง ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๊ณผ๊ฐํ ์ ๊ฑฐ
โข ๊ฐ๋ฅํ๋ฉด ์์กด์ฑ์ ์ต์ํ
์ค์ ๋ก ์ด๋ป๊ฒ ๊ด๋ฆฌํ๋์ง ๋ณด์! Node.js ํ๋ก์ ํธ ์์์ผ:
# 1. npm audit์ผ๋ก ์ทจ์ฝ์ ์ฒดํฌ
npm audit
# ์ถ๋ ฅ ์์:
# found 3 vulnerabilities (1 low, 2 high)
# run `npm audit fix` to fix them
# 2. ์๋์ผ๋ก ์์ ๊ฐ๋ฅํ ์ทจ์ฝ์ ํจ์น
npm audit fix
# 3. Breaking change๊ฐ ์๋ ๊ฒฝ์ฐ (์ฃผ์ ํ์!)
npm audit fix --force
# 4. ์์ธ ์ ๋ณด ํ์ธ
npm audit --json
# 5. package.json์ ๋ฒ์ ๋ฒ์ ๋ช
์
{
"dependencies": {
"express": "^4.18.2", // ^ ๊ธฐํธ: ๋ง์ด๋ ๋ฒ์ ๊น์ง ์๋ ์
๋ฐ์ดํธ
"mongoose": "~7.0.3" // ~ ๊ธฐํธ: ํจ์น ๋ฒ์ ๋ง ์๋ ์
๋ฐ์ดํธ
}
}
๋ ๊ฐ๋ ฅํ ๋๊ตฌ๋ค๋ ์์ด:
โข ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ์คํ์์ค ๋ณด์ ํ๋ซํผ
โข GitHub, GitLab ๋ฑ๊ณผ ํตํฉ ๊ฐ๋ฅ
โข ๋ฌด๋ฃ ํ๋ ์ ๊ณต
2. Dependabot (GitHub)
โข GitHub์ ๋ด์ฅ๋ ์์กด์ฑ ๊ด๋ฆฌ ๋๊ตฌ
โข ์ทจ์ฝ์ ๋ฐ๊ฒฌ ์ ์๋์ผ๋ก PR ์์ฑ
โข ์์ ๋ฌด๋ฃ!
3. npm audit / yarn audit
โข ํจํค์ง ๋งค๋์ ์ ๋ด์ฅ๋ ๊ธฐ๋ณธ ๋๊ตฌ
โข ๊ฐ๋จํ๊ณ ๋น ๋ฆ
4. OWASP Dependency-Check
โข ๋ค์ํ ์ธ์ด ์ง์ (Java, .NET, JavaScript ๋ฑ)
โข CI/CD ํตํฉ ๊ฐ๋ฅ
โข ์์ ๋ฌด๋ฃ ์คํ์์ค
GitHub Actions๋ฅผ ์ฌ์ฉํ ์๋ํ ์์:
# .github/workflows/security-scan.yml
name: Security Scan
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
# ๋งค์ฃผ ์์์ผ ์ค์ 9์์ ์๋ ์คํ
- cron: '0 9 * * 1'
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run npm audit
run: npm audit --audit-level=moderate
- name: Run Snyk test
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
โข ^1.2.3: 1.x.x ๋ฒ์ ๋ด์์ ์๋ ์ ๋ฐ์ดํธ (๋ง์ด๋, ํจ์น)
โข ~1.2.3: 1.2.x ๋ฒ์ ๋ด์์๋ง ์๋ ์ ๋ฐ์ดํธ (ํจ์น๋ง)
โข 1.2.3: ์ ํํ ์ด ๋ฒ์ ๋ง ์ฌ์ฉ
โข * ๋๋ latest: ์ต์ ๋ฒ์ (์ํ! ์ฌ์ฉ ์์ )
๋ณด์ ํจ์น๋ ๋น ๋ฅด๊ฒ ์ ์ฉํ๋, ๋ฉ์ด์ ๋ฒ์ ์ ๊ทธ๋ ์ด๋๋ ์ถฉ๋ถํ ํ ์คํธ ํ์ ํ๋ ๊ฒ ์ข์!
Python ํ๋ก์ ํธ๋ผ๋ฉด ์ด๋ ๊ฒ ํ ์ ์์ด:
# 1. Safety๋ก ์ทจ์ฝ์ ์ฒดํฌ
pip install safety
safety check
# 2. requirements.txt ์์ฑ (๋ฒ์ ๊ณ ์ )
pip freeze > requirements.txt
# 3. Pipenv ์ฌ์ฉ (๋ ๋์ ์์กด์ฑ ๊ด๋ฆฌ)
pip install pipenv
pipenv install express
pipenv check # ์ทจ์ฝ์ ์ฒดํฌ
# 4. Poetry ์ฌ์ฉ (์ต์ ๋๊ตฌ)
pip install poetry
poetry add express
poetry show --outdated # ์
๋ฐ์ดํธ ๊ฐ๋ฅํ ํจํค์ง ํ์ธ
๐ 7์: Identification and Authentication Failures (์๋ณ ๋ฐ ์ธ์ฆ ์คํจ)
์ด์ ์๋ "Broken Authentication"์ด๋ผ๊ณ ๋ถ๋ ธ๋ ์ทจ์ฝ์ ์ด์ผ. ์ฌ์ฉ์๊ฐ ๋๊ตฌ์ธ์ง ํ์ธํ๊ณ (์๋ณ), ๊ทธ ์ฌ๋์ด ๋ง๋์ง ๊ฒ์ฆํ๋(์ธ์ฆ) ๊ณผ์ ์์ ๋ฌธ์ ๊ฐ ์๊ธฐ๋ ๊ฑฐ์ง. ๐ญ
๐ค ์ด๋ค ๋ฌธ์ ๋ค์ด ์์๊น?
โข ์งง์ ๋น๋ฐ๋ฒํธ ํ์ฉ (์: 4์๋ฆฌ)
โข ๋จ์ํ ๋น๋ฐ๋ฒํธ ํ์ฉ (์: "1234", "password")
โข ๋น๋ฐ๋ฒํธ ๋ณต์ก๋ ์๊ตฌ์ฌํญ ์์
2. ๋ฌด์ฐจ๋ณ ๋์ ๊ณต๊ฒฉ(Brute Force) ๋ฐฉ์ด ๋ถ์กฑ
โข ๋ก๊ทธ์ธ ์๋ ํ์ ์ ํ ์์
โข CAPTCHA ์์
โข ๊ณ์ ์ ๊ธ ๊ธฐ๋ฅ ์์
3. ์ธ์ ๊ด๋ฆฌ ๋ฌธ์
โข ์ธ์ ID๊ฐ URL์ ๋ ธ์ถ
โข ๋ก๊ทธ์์ ํ์๋ ์ธ์ ์ด ์ ํจ
โข ์ธ์ ํ์์์์ด ๋๋ฌด ๊ธธ๊ฑฐ๋ ์์
4. ํฌ๋ฆฌ๋ด์ ์คํฐํ(Credential Stuffing)
โข ๋ค๋ฅธ ์ฌ์ดํธ์์ ์ ์ถ๋ ๊ณ์ ์ ๋ณด๋ก ๋ก๊ทธ์ธ ์๋
โข ๋ค์ค ์ธ์ฆ(MFA) ๋ฏธ์ ์ฉ
๐ฅ ์ค์ ๊ณต๊ฒฉ ์๋๋ฆฌ์ค
๊ณต๊ฒฉ์๊ฐ ์ด๋ป๊ฒ ์ธ์ฆ์ ์ฐํํ๋์ง ๋ณผ๊น?
1. ์ฌ์ฉ์๊ฐ ๊ณต๊ณต Wi-Fi์์ HTTP(์ํธํ ์ ๋จ)๋ก ๋ก๊ทธ์ธ
2. ๊ณต๊ฒฉ์๊ฐ ๋คํธ์ํฌ๋ฅผ ๊ฐ์ฒญํด์ ์ธ์ ์ฟ ํค ํ์ทจ
3. ๊ณต๊ฒฉ์๊ฐ ๊ทธ ์ฟ ํค๋ฅผ ์ฌ์ฉํด์ ์ฌ์ฉ์ ๊ณ์ ์ ์ ๊ทผ
์ด๋ฐ ๊ณต๊ฒฉ์ ๋ง์ผ๋ ค๋ฉด HTTPS ์ฌ์ฉ์ด ํ์์ผ! ๐
๐ก๏ธ ์์ ํ ์ธ์ฆ ๊ตฌํ
์ ๋๋ก ๋ ์ธ์ฆ ์์คํ ์ ๋ง๋ค์ด๋ณด์! JWT(JSON Web Token)๋ฅผ ์ฌ์ฉํ ์์์ผ:
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const rateLimit = require('express-rate-limit');
// 1. ๋ก๊ทธ์ธ ์๋ ํ์ ์ ํ
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 5, // ์ต๋ 5๋ฒ ์๋
message: '๋๋ฌด ๋ง์ ๋ก๊ทธ์ธ ์๋๊ฐ ์์์ต๋๋ค. 15๋ถ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.',
skipSuccessfulRequests: true // ์ฑ๊ณตํ ์์ฒญ์ ์นด์ดํธํ์ง ์์
});
// 2. ํ์๊ฐ์
- ๊ฐ๋ ฅํ ๋น๋ฐ๋ฒํธ ์ ์ฑ
async function register(req, res) {
const { username, email, password } = req.body;
// ๋น๋ฐ๋ฒํธ ๊ฐ๋ ๊ฒ์ฆ
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
if (!passwordRegex.test(password)) {
return res.status(400).json({
error: '๋น๋ฐ๋ฒํธ๋ ์ต์ 8์ ์ด์์ด๋ฉฐ, ๋๋ฌธ์, ์๋ฌธ์, ์ซ์, ํน์๋ฌธ์๋ฅผ ํฌํจํด์ผ ํฉ๋๋ค.'
});
}
// ๋น๋ฐ๋ฒํธ ํด์ฑ
const hashedPassword = await bcrypt.hash(password, 12);
// ์ฌ์ฉ์ ์์ฑ
const user = await User.create({
username,
email,
password: hashedPassword
});
res.json({ message: 'ํ์๊ฐ์
์ฑ๊ณต!' });
}
// 3. ๋ก๊ทธ์ธ
async function login(req, res) {
const { username, password } = req.body;
// ์ฌ์ฉ์ ์กฐํ
const user = await User.findOne({ username });
if (!user) {
// ๋ณด์์ ์ํด ๊ตฌ์ฒด์ ์ธ ์ ๋ณด๋ฅผ ์ฃผ์ง ์์
return res.status(401).json({ error: '์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์๋ชป๋์์ต๋๋ค.' });
}
// ๊ณ์ ์ ๊ธ ํ์ธ
if (user.lockUntil && user.lockUntil > Date.now()) {
return res.status(423).json({
error: '๊ณ์ ์ด ์ ๊ฒผ์ต๋๋ค. ๋์ค์ ๋ค์ ์๋ํด์ฃผ์ธ์.'
});
}
// ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
// ์คํจ ํ์ ์ฆ๊ฐ
user.loginAttempts = (user.loginAttempts || 0) + 1;
// 5๋ฒ ์คํจ ์ ๊ณ์ ์ ๊ธ (30๋ถ)
if (user.loginAttempts >= 5) {
user.lockUntil = Date.now() + 30 * 60 * 1000;
await user.save();
return res.status(423).json({
error: '๋ก๊ทธ์ธ ์๋ ํ์๋ฅผ ์ด๊ณผํ์ต๋๋ค. 30๋ถ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.'
});
}
await user.save();
return res.status(401).json({ error: '์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์๋ชป๋์์ต๋๋ค.' });
}
// ๋ก๊ทธ์ธ ์ฑ๊ณต - ์คํจ ํ์ ์ด๊ธฐํ
user.loginAttempts = 0;
user.lockUntil = null;
user.lastLogin = new Date();
await user.save();
// JWT ํ ํฐ ์์ฑ
const token = jwt.sign(
{
userId: user._id,
username: user.username,
role: user.role
},
process.env.JWT_SECRET,
{
expiresIn: '1h', // 1์๊ฐ ํ ๋ง๋ฃ
issuer: 'your-app-name',
audience: 'your-app-users'
}
);
// Refresh Token ์์ฑ (๋ ๊ธด ์ ํจ๊ธฐ๊ฐ)
const refreshToken = jwt.sign(
{ userId: user._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' } // 7์ผ
);
// Refresh Token์ DB์ ์ ์ฅ
user.refreshToken = refreshToken;
await user.save();
// HttpOnly ์ฟ ํค๋ก ์ ์ก (XSS ๋ฐฉ์ด)
res.cookie('refreshToken', refreshToken, {
httpOnly: true, // JavaScript๋ก ์ ๊ทผ ๋ถ๊ฐ
secure: true, // HTTPS์์๋ง ์ ์ก
sameSite: 'strict', // CSRF ๋ฐฉ์ด
maxAge: 7 * 24 * 60 * 60 * 1000 // 7์ผ
});
res.json({
token,
user: {
id: user._id,
username: user.username,
email: user.email
}
});
}
// 4. ํ ํฐ ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: '์ธ์ฆ ํ ํฐ์ด ํ์ํฉ๋๋ค.' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค.' });
}
return res.status(403).json({ error: '์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค.' });
}
req.user = user;
next();
});
}
// 5. ํ ํฐ ๊ฐฑ์
async function refreshAccessToken(req, res) {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token์ด ํ์ํฉ๋๋ค.' });
}
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await User.findById(decoded.userId);
// DB์ ์ ์ฅ๋ refresh token๊ณผ ๋น๊ต
if (user.refreshToken !== refreshToken) {
return res.status(403).json({ error: '์ ํจํ์ง ์์ refresh token์
๋๋ค.' });
}
// ์๋ก์ด access token ๋ฐ๊ธ
const newToken = jwt.sign(
{
userId: user._id,
username: user.username,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token: newToken });
} catch (err) {
return res.status(403).json({ error: '์ ํจํ์ง ์์ refresh token์
๋๋ค.' });
}
}
// 6. ๋ก๊ทธ์์
async function logout(req, res) {
const userId = req.user.userId;
// DB์์ refresh token ์ ๊ฑฐ
await User.findByIdAndUpdate(userId, { refreshToken: null });
// ์ฟ ํค ์ญ์
res.clearCookie('refreshToken');
res.json({ message: '๋ก๊ทธ์์ ๋์์ต๋๋ค.' });
}
// ๋ผ์ฐํธ ์ค์
app.post('/api/register', register);
app.post('/api/login', loginLimiter, login);
app.post('/api/refresh', refreshAccessToken);
app.post('/api/logout', authenticateToken, logout);
// ๋ณดํธ๋ ๋ผ์ฐํธ ์์
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
๋ ๊ฐ๋ ฅํ ๋ณด์์ ์ํด์๋ 2FA(Two-Factor Authentication)๋ MFA(Multi-Factor Authentication)๋ฅผ ๊ตฌํํ๋ ๊ฒ ์ข์. ๊ฐ์ฅ ํํ ๋ฐฉ๋ฒ์ TOTP(Time-based One-Time Password)๋ฅผ ์ฌ์ฉํ๋ ๊ฑฐ์ผ. Google Authenticator๋ Authy ๊ฐ์ ์ฑ์ด ์ด ๋ฐฉ์์ ์ฌ์ฉํด!
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// MFA ์ค์
async function setupMFA(req, res) {
const user = await User.findById(req.user.userId);
// Secret ํค ์์ฑ
const secret = speakeasy.generateSecret({
name: `YourApp (${user.email})`
});
// QR ์ฝ๋ ์์ฑ
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
// Secret์ ์์๋ก ์ ์ฅ (์ฌ์ฉ์๊ฐ ํ์ธ ํ ์ ์ฅ)
user.tempMFASecret = secret.base32;
await user.save();
res.json({
secret: secret.base32,
qrCode: qrCodeUrl
});
}
// MFA ํ์ธ ๋ฐ ํ์ฑํ
async function verifyMFA(req, res) {
const { token } = req.body;
const user = await User.findById(req.user.userId);
const verified = speakeasy.totp.verify({
secret: user.tempMFASecret,
encoding: 'base32',
token: token,
window: 2 // ์๊ฐ ์ค์ฐจ ํ์ฉ
});
if (verified) {
user.mfaSecret = user.tempMFASecret;
user.mfaEnabled = true;
user.tempMFASecret = null;
await user.save();
res.json({ message: 'MFA๊ฐ ํ์ฑํ๋์์ต๋๋ค!' });
} else {
res.status(400).json({ error: '์๋ชป๋ ์ฝ๋์
๋๋ค.' });
}
}
// ๋ก๊ทธ์ธ ์ MFA ๊ฒ์ฆ
async function loginWithMFA(req, res) {
// ... ๊ธฐ์กด ๋ก๊ทธ์ธ ๋ก์ง ...
if (user.mfaEnabled) {
// MFA ํ ํฐ ์๊ตฌ
const { mfaToken } = req.body;
if (!mfaToken) {
return res.status(200).json({
requireMFA: true,
message: 'MFA ์ฝ๋๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.'
});
}
const verified = speakeasy.totp.verify({
secret: user.mfaSecret,
encoding: 'base32',
token: mfaToken,
window: 2
});
if (!verified) {
return res.status(401).json({ error: '์๋ชป๋ MFA ์ฝ๋์
๋๋ค.' });
}
}
// JWT ํ ํฐ ๋ฐ๊ธ...
}
OAuth 2.0์ ์ฌ์ฉํ ์์ ๋ก๊ทธ์ธ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ์ฌ์ฉ์๋ ํธํ๊ณ , ๊ฐ๋ฐ์๋ ๋น๋ฐ๋ฒํธ ๊ด๋ฆฌ ๋ถ๋ด์ ๋ ์ ์์ง:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// Google OAuth ์ค์
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
try {
// ๊ธฐ์กด ์ฌ์ฉ์ ํ์ธ
let user = await User.findOne({ googleId: profile.id });
if (!user) {
// ์ ์ฌ์ฉ์ ์์ฑ
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
username: profile.displayName,
avatar: profile.photos[0].value
});
}
return done(null, user);
} catch (err) {
return done(err, null);
}
}
));
// ๋ผ์ฐํธ
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// JWT ํ ํฐ ๋ฐ๊ธ
const token = jwt.sign(
{ userId: req.user._id },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.redirect(`/dashboard?token=${token}`);
}
);
- โ ๊ฐ๋ ฅํ ๋น๋ฐ๋ฒํธ ์ ์ฑ ์ ์ฉ
- โ ๋น๋ฐ๋ฒํธ๋ bcrypt, Argon2 ๋ฑ์ผ๋ก ํด์ฑ
- โ ๋ก๊ทธ์ธ ์๋ ํ์ ์ ํ
- โ ๊ณ์ ์ ๊ธ ๊ธฐ๋ฅ ๊ตฌํ
- โ JWT ํ ํฐ ์ฌ์ฉ ์ ์งง์ ๋ง๋ฃ ์๊ฐ ์ค์
- โ Refresh Token ๊ตฌํ
- โ HttpOnly, Secure ์ฟ ํค ์ฌ์ฉ
- โ HTTPS ํ์ ์ฌ์ฉ
- โ ์ธ์ ํ์์์ ์ค์
- โ ๋ก๊ทธ์์ ์ ์ธ์ /ํ ํฐ ๋ฌดํจํ
- โ MFA(๋ค์ค ์ธ์ฆ) ์ ๊ณต
- โ ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ์ ์ด๋ฉ์ผ ์ธ์ฆ
- โ ๋ก๊ทธ์ธ ์๋ฆผ ๊ธฐ๋ฅ (์๋ก์ด ๊ธฐ๊ธฐ์์ ๋ก๊ทธ์ธ ์)
๐พ 8์: Software and Data Integrity Failures (์ํํธ์จ์ด ๋ฐ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ์คํจ)
2021๋ ์ ์๋กญ๊ฒ ์ถ๊ฐ๋ ์นดํ ๊ณ ๋ฆฌ์ผ! ์ฝ๋๋ ๋ฐ์ดํฐ๊ฐ ๋ณ์กฐ๋์ง ์์์์ ๋ณด์ฅํ์ง ๋ชปํ๋ ๋ฌธ์ ๋ฅผ ๋ค๋ค. ๐
๐ค ๋ฌด๊ฒฐ์ฑ ์คํจ๊ฐ ๋ญ์ผ?
๊ฐ๋จํ ๋งํ๋ฉด, ์ ๋ขฐํ ์ ์๋ ์์ค์์ ์ฝ๋๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ฑฐ๋, ๋ณ์กฐ ์ฌ๋ถ๋ฅผ ํ์ธํ์ง ์๋ ๊ฑฐ์ผ. ์๋ฅผ ๋ค๋ฉด:
โข ๋น๋ ๊ณผ์ ์์ ์ ์์ ์ธ ์ฝ๋ ์ฝ์ ๊ฐ๋ฅ
โข ๋ฐฐํฌ ๊ถํ ๊ด๋ฆฌ ๋ถ์กฑ
2. ์๋ ์ ๋ฐ์ดํธ ๋ฉ์ปค๋์ฆ ์ทจ์ฝ์
โข ์ ๋ฐ์ดํธ ํ์ผ์ ์๋ช ๊ฒ์ฆ ์์
โข ์ค๊ฐ์ ๊ณต๊ฒฉ(MITM)์ ์ทจ์ฝ
3. ์ ๋ขฐํ ์ ์๋ ์์ค์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
โข CDN์์ ๊ฐ์ ธ์จ JavaScript ํ์ผ ๊ฒ์ฆ ์์
โข npm ํจํค์ง์ ๋ฌด๊ฒฐ์ฑ ํ์ธ ์์
4. ์ง๋ ฌํ(Serialization) ์ทจ์ฝ์
โข ์ ๋ขฐํ ์ ์๋ ๋ฐ์ดํฐ๋ฅผ ์ญ์ง๋ ฌํ
โข ๊ฐ์ฒด ์ฃผ์ ๊ณต๊ฒฉ ๊ฐ๋ฅ
๐ฅ ์ค์ ์ฌ๋ก
๐ก๏ธ ๋์ ๋ฐฉ๋ฒ
๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ๋ ๋ฐฉ๋ฒ๋ค์ ์์๋ณด์!
CDN์์ ๊ฐ์ ธ์ค๋ ํ์ผ์ ํด์๊ฐ์ ๊ฒ์ฆํด.
2. ๋์งํธ ์๋ช ๊ฒ์ฆ
์ํํธ์จ์ด ์ ๋ฐ์ดํธ๋ ํจํค์ง์ ์๋ช ์ ํ์ธํด.
3. ์์ ํ CI/CD ํ์ดํ๋ผ์ธ
๋น๋ ๊ณผ์ ์ ๊ฒฉ๋ฆฌํ๊ณ , ์ ๊ทผ ๊ถํ์ ์๊ฒฉํ ๊ด๋ฆฌํด.
4. ์ญ์ง๋ ฌํ ๋ณด์
์ ๋ขฐํ ์ ์๋ ๋ฐ์ดํฐ๋ ์ญ์ง๋ ฌํํ์ง ๋ง!
์ค์ ์ฝ๋๋ก ๋ณด์. ๋จผ์ SRI(Subresource Integrity)๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด์ผ:
<!-- โ ์ํํ ๋ฐฉ๋ฒ - ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ์์ -->
<script src="https://cdn.example.com/library.js"></script>
<!-- โ
์์ ํ ๋ฐฉ๋ฒ - SRI ์ฌ์ฉ -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>
<!-- ์ฌ๋ฌ ํด์ ์๊ณ ๋ฆฌ์ฆ ์ฌ์ฉ ๊ฐ๋ฅ -->
<script
src="https://cdn.example.com/library.js"
integrity="sha256-abc123... sha384-def456... sha512-ghi789..."
crossorigin="anonymous">
</script>
SRI ํด์๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ:
# ๋ช
๋ น์ค์์ ์์ฑ
cat library.js | openssl dgst -sha384 -binary | openssl base64 -A
# Node.js๋ก ์์ฑ
const crypto = require('crypto');
const fs = require('fs');
function generateSRI(filePath, algorithm = 'sha384') {
const fileBuffer = fs.readFileSync(filePath);
const hash = crypto.createHash(algorithm).update(fileBuffer).digest('base64');
return `${algorithm}-${hash}`;
}
const integrity = generateSRI('./library.js');
console.log(integrity);
// ์ถ๋ ฅ: sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC
npm ํจํค์ง์ ๋ฌด๊ฒฐ์ฑ์ ํ์ธํ๋ ๋ฐฉ๋ฒ:
# package-lock.json์ ๋ฌด๊ฒฐ์ฑ ํด์๊ฐ ํฌํจ๋จ
{
"name": "express",
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"dependencies": {
...
}
}
# npm ci ๋ช
๋ น์ด๋ package-lock.json์ ๋ฌด๊ฒฐ์ฑ์ ๊ฒ์ฆ
npm ci # ํ๋ก๋์
ํ๊ฒฝ์์ ๊ถ์ฅ
# ํน์ ํจํค์ง์ ๋ฌด๊ฒฐ์ฑ ํ์ธ
npm audit
์ญ์ง๋ ฌํ ๋ณด์ ์์:
// โ ์ํํ ์ฝ๋ - ์ ๋ขฐํ ์ ์๋ ๋ฐ์ดํฐ ์ญ์ง๋ ฌํ
const userInput = req.body.data;
const obj = JSON.parse(userInput); // ์ํ!
// ๋ง์ฝ ์ฌ์ฉ์๊ฐ ์ด๋ฐ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ธ๋ค๋ฉด?
// {"__proto__": {"isAdmin": true}}
// ํ๋กํ ํ์
์ค์ผ(Prototype Pollution) ๊ณต๊ฒฉ ๊ฐ๋ฅ!
// โ
์์ ํ ์ฝ๋ - ๊ฒ์ฆ ํ ์ญ์ง๋ ฌํ
const Joi = require('joi');
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(120)
});
async function handleUserData(req, res) {
try {
// 1. ์คํค๋ง ๊ฒ์ฆ
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
// 2. ์์ ํ๊ฒ ์ฌ์ฉ
const user = {
username: value.username,
email: value.email,
age: value.age
};
// 3. ํ๋กํ ํ์
์ค์ผ ๋ฐฉ์ง
Object.freeze(user);
res.json({ success: true, user });
} catch (err) {
res.status(500).json({ error: '์๋ฒ ์ค๋ฅ' });
}
}
// Python์์ pickle ์ฌ์ฉ ์ ์ฃผ์
# โ ์ํ
import pickle
data = pickle.loads(untrusted_data) # ์์ ์ฝ๋ ์คํ ๊ฐ๋ฅ!
# โ
์์
import json
data = json.loads(untrusted_data) # JSON์ ๋ ์์
# ๋๋ hmac์ผ๋ก ์๋ช
๊ฒ์ฆ
CI/CD ํ์ดํ๋ผ์ธ ๋ณด์:
# GitHub Actions ๋ณด์ ์ค์ ์์
name: Secure CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
# 1. ์ต์ ๊ถํ ์์น
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
with:
# 2. ์ปค๋ฐ ์๋ช
๊ฒ์ฆ
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
# 3. ์์กด์ฑ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ
- name: Install dependencies
run: npm ci # npm install ๋์ npm ci ์ฌ์ฉ
# 4. ๋ณด์ ์ค์บ
- name: Run security audit
run: npm audit --audit-level=moderate
# 5. ์ฝ๋ ์๋ช
- name: Sign artifacts
run: |
gpg --import ${{ secrets.GPG_PRIVATE_KEY }}
gpg --detach-sign --armor dist/app.js
# 6. ๋น๋ ์ํฐํฉํธ ์
๋ก๋ (ํด์ ํฌํจ)
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: build-artifacts
path: |
dist/
dist/app.js.asc
retention-days: 30
# 7. ๋ฐฐํฌ ์น์ธ ํ์
- name: Deploy to production
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://example.com
run: |
# ๋ฐฐํฌ ์คํฌ๋ฆฝํธ
./deploy.sh
โข GPG ํค๋ฅผ ์ฌ์ฉํด์ ์ปค๋ฐ์ ์๋ช ํ๊ธฐ
โข ๋ฆด๋ฆฌ์ค ์ํฐํฉํธ์ ๋์งํธ ์๋ช ์ถ๊ฐ
โข ์๋ช ๊ฒ์ฆ์ ์๋ํํ๊ธฐ
โข ํค ๊ด๋ฆฌ๋ฅผ ์์ ํ๊ฒ (HSM, Key Vault ๋ฑ ์ฌ์ฉ)
# Git ์ปค๋ฐ ์๋ช
์ค์
git config --global user.signingkey YOUR_GPG_KEY_ID
git config --global commit.gpgsign true
# ์๋ช
๋ ์ปค๋ฐ ์์ฑ
git commit -S -m "Signed commit"
# ์๋ช
๊ฒ์ฆ
git log --show-signature
๐ 9์: Security Logging and Monitoring Failures (๋ณด์ ๋ก๊น ๋ฐ ๋ชจ๋ํฐ๋ง ์คํจ)
๊ณต๊ฒฉ์ ๋ฐ๊ณ ์๋๋ฐ ๋ชจ๋ฅด๊ณ ์๋ค๋ฉด? ๊ทธ๊ฒ ๋ฐ๋ก ์ด ์ทจ์ฝ์ ์ด์ผ! ๋ก๊ทธ๋ฅผ ์ ๋๋ก ๋จ๊ธฐ์ง ์๊ฑฐ๋, ๋ชจ๋ํฐ๋ง์ ํ์ง ์์ผ๋ฉด ๊ณต๊ฒฉ์ ํ์งํ ์ ์์ด. ๐
๐ค ์ ๋ก๊น ๊ณผ ๋ชจ๋ํฐ๋ง์ด ์ค์ํ ๊น?
โข ๋ฐ๊ฒฌ ํ ์์ ํ ํด๊ฒฐํ๋ ๋ฐ ์ถ๊ฐ๋ก 73์ผ ์์
โข ์ฆ, ๊ณต๊ฒฉ์๊ฐ ์ฝ 9๊ฐ์ ๋์ ์์คํ ์ ์ ๊ทผํ ์ ์๋ค๋ ๋ป!
โข ์กฐ๊ธฐ์ ๋ฐ๊ฒฌํ๋ฉด ํผํด ๋น์ฉ์ ํ๊ท 100๋ง ๋ฌ๋ฌ ์ ๊ฐ ๊ฐ๋ฅ
๐จ ํํ ๋ก๊น ์ค์๋ค
โข ๋ก๊ทธ์ธ ์๋ (์ฑ๊ณต/์คํจ)
โข ๊ถํ ๋ณ๊ฒฝ
โข ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ ๊ทผ
โข ์ค์ ๋ณ๊ฒฝ
2. ๋ก๊ทธ์ ๋ฏผ๊ฐํ ์ ๋ณด ํฌํจ
โข ๋น๋ฐ๋ฒํธ, ํ ํฐ, API ํค
โข ์ ์ฉ์นด๋ ๋ฒํธ
โข ๊ฐ์ธ์ ๋ณด
3. ๋ก๊ทธ๋ฅผ ๋ณดํธํ์ง ์์
โข ๋๊ตฌ๋ ๋ก๊ทธ ํ์ผ์ ์ ๊ทผ ๊ฐ๋ฅ
โข ๋ก๊ทธ ๋ณ์กฐ ๊ฐ๋ฅ
โข ๋ฐฑ์ ์์
4. ์๋ฆผ ์์คํ ๋ถ์ฌ
โข ์ด์ ์งํ ํ์ง ์ ๋จ
โข ์ค์๊ฐ ์๋ฆผ ์์
๐ก๏ธ ์ฌ๋ฐ๋ฅธ ๋ก๊น ๊ตฌํ
์ ๋๋ก ๋ ๋ก๊น ์์คํ ์ ๋ง๋ค์ด๋ณด์! Winston์ ์ฌ์ฉํ Node.js ์์์ผ:
const winston = require('winston');
const { format } = winston;
// 1. ๋ก๊ฑฐ ์ค์
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: format.combine(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: { service: 'user-service' },
transports: [
// ์๋ฌ ๋ก๊ทธ๋ ๋ณ๋ ํ์ผ์
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// ๋ชจ๋ ๋ก๊ทธ
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 5242880,
maxFiles: 5
})
]
});
// ๊ฐ๋ฐ ํ๊ฒฝ์์๋ ์ฝ์์๋ ์ถ๋ ฅ
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: format.combine(
format.colorize(),
format.simple()
)
}));
}
// 2. ๋ณด์ ์ด๋ฒคํธ ๋ก๊น
ํจ์๋ค
function logLoginAttempt(username, success, ip, userAgent) {
logger.info('Login attempt', {
event: 'login_attempt',
username: username,
success: success,
ip: ip,
userAgent: userAgent,
timestamp: new Date().toISOString()
});
}
function logLoginSuccess(userId, username, ip) {
logger.info('Login successful', {
event: 'login_success',
userId: userId,
username: username,
ip: ip,
timestamp: new Date().toISOString()
});
}
function logLoginFailure(username, reason, ip) {
logger.warn('Login failed', {
event: 'login_failure',
username: username,
reason: reason,
ip: ip,
timestamp: new Date().toISOString()
});
}
function logSuspiciousActivity(userId, activity, details) {
logger.warn('Suspicious activity detected', {
event: 'suspicious_activity',
userId: userId,
activity: activity,
details: details,
timestamp: new Date().toISOString()
});
}
function logDataAccess(userId, resource, action) {
logger.info('Data access', {
event: 'data_access',
userId: userId,
resource: resource,
action: action,
timestamp: new Date().toISOString()
});
}
function logSecurityEvent(eventType, severity, details) {
logger.log(severity, 'Security event', {
event: eventType,
severity: severity,
details: details,
timestamp: new Date().toISOString()
});
}
// 3. ๋ฏผ๊ฐํ ์ ๋ณด ๋ง์คํน
function maskSensitiveData(data) {
const masked = { ...data };
// ๋น๋ฐ๋ฒํธ ๋ง์คํน
if (masked.password) {
masked.password = '***MASKED***';
}
// ์ด๋ฉ์ผ ๋ถ๋ถ ๋ง์คํน
if (masked.email) {
const [local, domain] = masked.email.split('@');
masked.email = `${local.substring(0, 2)}***@${domain}`;
}
// ์ ์ฉ์นด๋ ๋ฒํธ ๋ง์คํน
if (masked.cardNumber) {
masked.cardNumber = `****-****-****-${masked.cardNumber.slice(-4)}`;
}
return masked;
}
// 4. ๋ก๊ทธ์ธ ๋ฏธ๋ค์จ์ด
function logRequest(req, res, next) {
const start = Date.now();
// ์๋ต ์๋ฃ ์ ๋ก๊น
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent'),
userId: req.user?.id
});
// ๋๋ฆฐ ์์ฒญ ๊ฒฝ๊ณ
if (duration > 3000) {
logger.warn('Slow request detected', {
method: req.method,
url: req.url,
duration: `${duration}ms`
});
}
// ์๋ฌ ์๋ต ๋ก๊น
if (res.statusCode >= 400) {
logger.warn('Error response', {
method: req.method,
url: req.url,
status: res.statusCode,
ip: req.ip
});
}
});
next();
}
// 5. ์๋ฌ ํธ๋ค๋ง ๋ฏธ๋ค์จ์ด
function errorLogger(err, req, res, next) {
logger.error('Application error', {
error: err.message,
stack: err.stack,
method: req.method,
url: req.url,
ip: req.ip,
userId: req.user?.id
});
next(err);
}
// 6. ์ค์ ์ฌ์ฉ ์์
app.use(logRequest);
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
const ip = req.ip;
const userAgent = req.get('user-agent');
try {
const user = await User.findOne({ username });
if (!user) {
logLoginFailure(username, 'User not found', ip);
return res.status(401).json({ error: '๋ก๊ทธ์ธ ์คํจ' });
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
logLoginFailure(username, 'Invalid password', ip);
// ์ฐ์ ์คํจ ๊ฐ์ง
user.failedLoginAttempts = (user.failedLoginAttempts || 0) + 1;
if (user.failedLoginAttempts >= 5) {
logSuspiciousActivity(user.id, 'Multiple failed login attempts', {
attempts: user.failedLoginAttempts,
ip: ip
});
}
await user.save();
return res.status(401).json({ error: '๋ก๊ทธ์ธ ์คํจ' });
}
// ๋ก๊ทธ์ธ ์ฑ๊ณต
logLoginSuccess(user.id, username, ip);
// ์๋ก์ด ๊ธฐ๊ธฐ์์ ๋ก๊ทธ์ธ ์ ์๋ฆผ
if (!user.knownDevices.includes(userAgent)) {
logSecurityEvent('new_device_login', 'info', {
userId: user.id,
ip: ip,
userAgent: userAgent
});
// ์ด๋ฉ์ผ ์๋ฆผ ์ ์ก
sendSecurityAlert(user.email, 'New device login detected');
}
user.failedLoginAttempts = 0;
user.lastLogin = new Date();
await user.save();
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
res.json({ token });
} catch (err) {
logger.error('Login error', { error: err.message, username });
res.status(500).json({ error: '์๋ฒ ์ค๋ฅ' });
}
});
app.use(errorLogger);
module.exports = { logger, logRequest, errorLogger };
โข ELK Stack (Elasticsearch, Logstash, Kibana): ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ์คํ์์ค ๋ก๊ทธ ๋ถ์ ํ๋ซํผ
โข Splunk: ๊ฐ๋ ฅํ ์์ฉ ๋ก๊ทธ ๋ถ์ ๋๊ตฌ
โข Graylog: ์คํ์์ค ๋ก๊ทธ ๊ด๋ฆฌ ํ๋ซํผ
โข Datadog: ํด๋ผ์ฐ๋ ๊ธฐ๋ฐ ๋ชจ๋ํฐ๋ง ์๋น์ค
โข Sentry: ์๋ฌ ์ถ์ ๋ฐ ๋ชจ๋ํฐ๋ง
์ค์๊ฐ ์๋ฆผ ์์คํ ๊ตฌํ:
const nodemailer = require('nodemailer');
const slack = require('@slack/webhook');
// ์ด๋ฉ์ผ ์๋ฆผ
const emailTransporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
}
});
async function sendSecurityAlert(to, subject, details) {
const mailOptions = {
from: 'security@yourapp.com',
to: to,
subject: `[๋ณด์ ์๋ฆผ] ${subject}`,
html: `
<h2>๋ณด์ ์๋ฆผ</h2>
<p>${subject}</p>
<pre>${JSON.stringify(details, null, 2)}</pre>
<p>์ด ํ๋์ด ๋ณธ์ธ์ด ์๋๋ผ๋ฉด ์ฆ์ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ณ๊ฒฝํด์ฃผ์ธ์.</p>
`
};
try {
await emailTransporter.sendMail(mailOptions);
logger.info('Security alert sent', { to, subject });
} catch (err) {
logger.error('Failed to send security alert', { error: err.message });
}
}
// Slack ์๋ฆผ
const slackWebhook = new slack.IncomingWebhook(process.env.SLACK_WEBHOOK_URL);
async function sendSlackAlert(message, severity = 'warning') {
const colors = {
info: '#36a64f',
warning: '#ff9900',
error: '#ff0000'
};
try {
await slackWebhook.send({
attachments: [{
color: colors[severity],
title: '๋ณด์ ์๋ฆผ',
text: message,
timestamp: Math.floor(Date.now() / 1000)
}]
});
} catch (err) {
logger.error('Failed to send Slack alert', { error: err.message });
}
}
// ์ด์ ์งํ ํ์ง
function detectAnomalies() {
// ์: ์งง์ ์๊ฐ์ ๋ง์ ๋ก๊ทธ์ธ ์คํจ
const recentFailures = getRecentLoginFailures(5); // ์ต๊ทผ 5๋ถ
if (recentFailures.length > 10) {
const message = `โ ๏ธ ๋น์ ์์ ์ธ ๋ก๊ทธ์ธ ์๋ ๊ฐ์ง!\n` +
`์ต๊ทผ 5๋ถ๊ฐ ${recentFailures.length}๋ฒ์ ์คํจํ ๋ก๊ทธ์ธ ์๋`;
sendSlackAlert(message, 'error');
logSecurityEvent('brute_force_detected', 'error', {
attempts: recentFailures.length,
timeWindow: '5 minutes'
});
}
}
// ์ ๊ธฐ์ ์ผ๋ก ์ด์ ์งํ ์ฒดํฌ
setInterval(detectAnomalies, 60000); // 1๋ถ๋ง๋ค
๐ 10์: Server-Side Request Forgery (SSRF)
๋ง์ง๋ง 10์๋ SSRF์ผ! ์ด๊ฑด ๊ณต๊ฒฉ์๊ฐ ์๋ฒ๋ฅผ ์ด์ฉํด์ ๋ด๋ถ ๋คํธ์ํฌ๋ ๋ค๋ฅธ ์์คํ ์ ์์ฒญ์ ๋ณด๋ด๋๋ก ๋ง๋๋ ๊ณต๊ฒฉ์ด์ผ. ๐ฏ
๐ค SSRF๊ฐ ๋ญ๊ธธ๋?
๊ฐ๋จํ ์์๋ก ์ดํดํด๋ณด์:
๐ฅ ๊ณต๊ฒฉ ์๋๋ฆฌ์ค
// ์ทจ์ฝํ ์ฝ๋ ์์
app.get('/api/fetch-image', async (req, res) => {
const imageUrl = req.query.url;
// ์ฌ์ฉ์๊ฐ ์ ๊ณตํ URL์์ ์ด๋ฏธ์ง ๊ฐ์ ธ์ค๊ธฐ
const response = await fetch(imageUrl);
const imageData = await response.buffer();
res.send(imageData);
});
// ๊ณต๊ฒฉ์๊ฐ ์ด๋ ๊ฒ ์์ฒญํ๋ฉด?
GET /api/fetch-image?url=http://localhost:8080/admin
// ๋๋ ๋ด๋ถ ๋ฉํ๋ฐ์ดํฐ ์๋น์ค ์ ๊ทผ
GET /api/fetch-image?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
// AWS EC2 ์ธ์คํด์ค์ IAM ํฌ๋ ๋ด์
์ ํ์ทจํ ์ ์์ด! ๐ฑ
๐ก๏ธ SSRF ๋ฐฉ์ด ๋ฐฉ๋ฒ
ํ์ฉ๋ ๋๋ฉ์ธ๋ง ์ ๊ทผ ๊ฐ๋ฅํ๋๋ก ์ ํ
2. ๋ด๋ถ IP ์ฐจ๋จ
localhost, ์ฌ์ค IP ๋์ญ ์ฐจ๋จ
3. URL ํ์ฑ ๋ฐ ๊ฒ์ฆ
URL์ ํ์ฑํด์ ํ๋กํ ์ฝ, ํธ์คํธ, ํฌํธ ๊ฒ์ฆ
4. ๋คํธ์ํฌ ๋ ๋ฒจ ๊ฒฉ๋ฆฌ
์ ํ๋ฆฌ์ผ์ด์ ์๋ฒ๋ฅผ DMZ์ ๋ฐฐ์น
์์ ํ ์ฝ๋ ๊ตฌํ:
const url = require('url');
const dns = require('dns').promises;
const ipaddr = require('ipaddr.js');
// 1. URL ๊ฒ์ฆ ํจ์
async function isUrlSafe(urlString) {
try {
const parsedUrl = new URL(urlString);
// ํ๋กํ ์ฝ ๊ฒ์ฆ (HTTP/HTTPS๋ง ํ์ฉ)
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
return { safe: false, reason: 'Invalid protocol' };
}
// ํฌํธ ๊ฒ์ฆ (์ผ๋ฐ์ ์ธ ํฌํธ๋ง ํ์ฉ)
const port = parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80);
const allowedPorts = [80, 443, 8080, 8443];
if (!allowedPorts.includes(parseInt(port))) {
return { safe: false, reason: 'Invalid port' };
}
// ํธ์คํธ๋ค์ ๊ฒ์ฆ
const hostname = parsedUrl.hostname;
// localhost ์ฐจ๋จ
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
return { safe: false, reason: 'Localhost not allowed' };
}
// DNS ์กฐํ
const addresses = await dns.resolve4(hostname).catch(() => []);
// IP ์ฃผ์ ๊ฒ์ฆ
for (const address of addresses) {
const addr = ipaddr.parse(address);
// ์ฌ์ค IP ๋์ญ ์ฐจ๋จ
if (addr.range() === 'private' ||
addr.range() === 'loopback' ||
addr.range() === 'linkLocal') {
return { safe: false, reason: 'Private IP not allowed' };
}
// ํน์ IP ๋์ญ ์ฐจ๋จ
// 169.254.0.0/16 (AWS ๋ฉํ๋ฐ์ดํฐ)
// 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (์ฌ์ค IP)
if (address.startsWith('169.254.') ||
address.startsWith('10.') ||
address.match(/^172\.(1[6-9]|2[0-9]|3[01])\./) ||
address.startsWith('192.168.')) {
return { safe: false, reason: 'Restricted IP range' };
}
}
return { safe: true };
} catch (err) {
return { safe: false, reason: 'Invalid URL' };
}
}
// 2. ํ์ดํธ๋ฆฌ์คํธ ๋ฐฉ์
const ALLOWED_DOMAINS = [
'api.example.com',
'cdn.example.com',
'images.example.com'
];
function isDomainAllowed(hostname) {
return ALLOWED_DOMAINS.some(domain =>
hostname === domain || hostname.endsWith(`.${domain}`)
);
}
// 3. ์์ ํ fetch ํจ์
async function safeFetch(urlString, options = {}) {
// URL ๊ฒ์ฆ
const validation = await isUrlSafe(urlString);
if (!validation.safe) {
throw new Error(`URL not allowed: ${validation.reason}`);
}
const parsedUrl = new URL(urlString);
// ๋๋ฉ์ธ ํ์ดํธ๋ฆฌ์คํธ ์ฒดํฌ (์ ํ์ )
if (!isDomainAllowed(parsedUrl.hostname)) {
throw new Error('Domain not in whitelist');
}
// ํ์์์ ์ค์ (๋๋ฆฐ ์๋ต ๋ฐฉ์ง)
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000); // 5์ด
try {
const response = await fetch(urlString, {
...options,
signal: controller.signal,
// ๋ฆฌ๋ค์ด๋ ํธ ์ ํ
redirect: 'manual',
// ์ต๋ ์๋ต ํฌ๊ธฐ ์ ํ์ ๋ณ๋๋ก ์ฒ๋ฆฌ
});
clearTimeout(timeout);
// ๋ฆฌ๋ค์ด๋ ํธ ์ฒดํฌ
if (response.status >= 300 && response.status < 400) {
const redirectUrl = response.headers.get('location');
// ๋ฆฌ๋ค์ด๋ ํธ URL๋ ๊ฒ์ฆ
const redirectValidation = await isUrlSafe(redirectUrl);
if (!redirectValidation.safe) {
throw new Error('Redirect URL not allowed');
}
}
return response;
} catch (err) {
clearTimeout(timeout);
throw err;
}
}
// 4. ์ค์ ์ฌ์ฉ ์์
app.get('/api/fetch-image', async (req, res) => {
const imageUrl = req.query.url;
if (!imageUrl) {
return res.status(400).json({ error: 'URL is required' });
}
try {
// ์์ ํ fetch ์ฌ์ฉ
const response = await safeFetch(imageUrl);
// Content-Type ๊ฒ์ฆ
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.startsWith('image/')) {
return res.status(400).json({ error: 'Not an image' });
}
// ํฌ๊ธฐ ์ ํ (10MB)
const contentLength = response.headers.get('content-length');
if (contentLength && parseInt(contentLength) > 10 * 1024 * 1024) {
return res.status(400).json({ error: 'Image too large' });
}
const imageData = await response.buffer();
res.set('Content-Type', contentType);
res.send(imageData);
} catch (err) {
logger.error('Fetch image error', { error: err.message, url: imageUrl });
res.status(400).json({ error: 'Failed to fetch image' });
}
});
// 5. Webhook URL ๊ฒ์ฆ ์์
async function validateWebhookUrl(webhookUrl) {
const validation = await isUrlSafe(webhookUrl);
if (!validation.safe) {
throw new Error(`Invalid webhook URL: ${validation.reason}`);
}
// ์ถ๊ฐ ๊ฒ์ฆ: HTTPS ํ์
const parsedUrl = new URL(webhookUrl);
if (parsedUrl.protocol !== 'https:') {
throw new Error('Webhook URL must use HTTPS');
}
return true;
}
app.post('/api/webhooks', async (req, res) => {
const { url, events } = req.body;
try {
await validateWebhookUrl(url);
// Webhook ๋ฑ๋ก
const webhook = await Webhook.create({
userId: req.user.id,
url: url,
events: events
});
res.json({ webhook });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
โข ๋คํธ์ํฌ ์ธ๊ทธ๋ฉํ ์ด์ : ์ ํ๋ฆฌ์ผ์ด์ ์๋ฒ๋ฅผ DMZ์ ๋ฐฐ์นํ๊ณ , ๋ด๋ถ ๋คํธ์ํฌ์ ๊ฒฉ๋ฆฌ
โข ๋ฐฉํ๋ฒฝ ๊ท์น: ์์๋ฐ์ด๋ ํธ๋ํฝ์ ์ ํํ๊ณ , ํ์ํ ๋๋ฉ์ธ๋ง ํ์ฉ
โข ํ๋ก์ ์๋ฒ: ๋ชจ๋ ์ธ๋ถ ์์ฒญ์ ํ๋ก์๋ฅผ ํตํด ์ฒ๋ฆฌํ๊ณ , ํ๋ก์์์ ํํฐ๋ง
โข AWS IMDSv2: AWS ํ๊ฒฝ์ด๋ผ๋ฉด IMDSv2๋ฅผ ์ฌ์ฉํด์ ๋ฉํ๋ฐ์ดํฐ ์๋น์ค ๋ณดํธ
AWS IMDSv2 ์ค์ ์์:
# EC2 ์ธ์คํด์ค์์ IMDSv2 ๊ฐ์
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1
# ์ ํ๋ฆฌ์ผ์ด์
์์ IMDSv2 ์ฌ์ฉ
const axios = require('axios');
async function getEC2Metadata() {
try {
// 1. ํ ํฐ ์์ฒญ
const tokenResponse = await axios.put(
'http://169.254.169.254/latest/api/token',
null,
{
headers: {
'X-aws-ec2-metadata-token-ttl-seconds': '21600'
}
}
);
const token = tokenResponse.data;
// 2. ํ ํฐ์ ์ฌ์ฉํด์ ๋ฉํ๋ฐ์ดํฐ ์์ฒญ
const metadataResponse = await axios.get(
'http://169.254.169.254/latest/meta-data/instance-id',
{
headers: {
'X-aws-ec2-metadata-token': token
}
}
);
return metadataResponse.data;
} catch (err) {
console.error('Failed to get metadata:', err.message);
return null;
}
}
๐ฏ ๋ณด์ ๊ฐํ๋ฅผ ์ํ ์ข ํฉ ์ ๋ต
์, ์ด์ OWASP Top 10์ ๋ชจ๋ ์ดํด๋ดค์ด! ํ์ง๋ง ์ด๊ฑธ ์ด๋ป๊ฒ ์ค์ ๋ก ์ ์ฉํ ๊น? ์ข ํฉ์ ์ธ ๋ณด์ ์ ๋ต์ ์ธ์๋ณด์! ๐
๐ ๋ณด์ ๊ฐ๋ฐ ์๋ช ์ฃผ๊ธฐ (SDL)
โข ๋ณด์ ์๊ตฌ์ฌํญ ์ ์
โข ๊ท์ ์ค์ ์ฌํญ ํ์ธ (GDPR, PCI-DSS ๋ฑ)
โข ๊ฐ์ธ์ ๋ณด ์ํฅ ํ๊ฐ
2. ์ค๊ณ ๋จ๊ณ
โข ์ํ ๋ชจ๋ธ๋ง (Threat Modeling)
โข ๋ณด์ ์ํคํ ์ฒ ๋ฆฌ๋ทฐ
โข ๋ฐ์ดํฐ ํ๋ฆ ๋ถ์
3. ๊ฐ๋ฐ ๋จ๊ณ
โข ๋ณด์ ์ฝ๋ฉ ๊ฐ์ด๋๋ผ์ธ ์ค์
โข ์ฝ๋ ๋ฆฌ๋ทฐ (๋ณด์ ๊ด์ )
โข ์ ์ ๋ถ์ ๋๊ตฌ ์ฌ์ฉ (SAST)
4. ํ ์คํธ ๋จ๊ณ
โข ๋์ ๋ถ์ ๋๊ตฌ ์ฌ์ฉ (DAST)
โข ์นจํฌ ํ ์คํธ (Penetration Testing)
โข ์ทจ์ฝ์ ์ค์บ
5. ๋ฐฐํฌ ๋จ๊ณ
โข ๋ณด์ ์ค์ ๊ฒ์ฆ
โข ์ต์ ๊ถํ ์์น ์ ์ฉ
โข ๋ณด์ ๋ชจ๋ํฐ๋ง ์ค์
6. ์ด์ ๋จ๊ณ
โข ์ ๊ธฐ์ ์ธ ๋ณด์ ํจ์น
โข ๋ก๊ทธ ๋ชจ๋ํฐ๋ง
โข ์ธ์๋ํธ ๋์ ๊ณํ
๐ ๏ธ ํ์ ๋ณด์ ๋๊ตฌ๋ค
| ์นดํ ๊ณ ๋ฆฌ | ๋๊ตฌ | ์ฉ๋ |
|---|---|---|
| SAST | SonarQube, Checkmarx | ์์ค ์ฝ๋ ์ ์ ๋ถ์ |
| DAST | OWASP ZAP, Burp Suite | ์คํ ์ค์ธ ์ฑ ๋์ ๋ถ์ |
| SCA | Snyk, WhiteSource | ์คํ์์ค ์ทจ์ฝ์ ์ค์บ |
| Container Security | Trivy, Clair | ์ปจํ ์ด๋ ์ด๋ฏธ์ง ์ค์บ |
| Secrets Management | HashiCorp Vault, AWS Secrets Manager | ๋น๋ฐ ์ ๋ณด ๊ด๋ฆฌ |
| WAF | Cloudflare, AWS WAF | ์น ์ ํ๋ฆฌ์ผ์ด์ ๋ฐฉํ๋ฒฝ |
| Monitoring | Datadog, New Relic | ๋ณด์ ๋ชจ๋ํฐ๋ง |
๐ ๋ณด์ ํ์ต ๋ฆฌ์์ค
โข OWASP WebGoat: ์ค์ต์ผ๋ก ๋ฐฐ์ฐ๋ ์น ๋ณด์
โข HackTheBox: ์ค์ ํดํน ์ฐ์ต ํ๋ซํผ
โข PortSwigger Web Security Academy: ๋ฌด๋ฃ ์น ๋ณด์ ๊ฐ์ข
โข TryHackMe: ์ด๋ณด์ ์นํ์ ์ธ ๋ณด์ ํ์ต
์๊ฒฉ์ฆ
โข CEH (Certified Ethical Hacker)
โข OSCP (Offensive Security Certified Professional)
โข CISSP (Certified Information Systems Security Professional)
โข Security+ (CompTIA)
์ปค๋ฎค๋ํฐ
โข OWASP ํ๊ตญ ์ฑํฐ
โข ์ ๋ณด๋ณด์ ๊ด๋ จ ์ปจํผ๋ฐ์ค (CodeEngn, HITCON ๋ฑ)
โข Bug Bounty ํ๋ก๊ทธ๋จ (HackerOne, Bugcrowd)
โข ๋ณด์ ๊ฐ์ฌ ๋ฐ ์ทจ์ฝ์ ์ง๋จ
โข ์นจํฌ ํ ์คํธ
โข ๋ณด์ ์ํคํ ์ฒ ์ปจ์คํ
โข ๋ณด์ ๊ต์ก ๋ฐ ํธ๋ ์ด๋
โข ์ธ์๋ํธ ๋์ ์ง์
ํนํ ์ค์ํ ํ๋ก์ ํธ๋ ๊ธ์ต, ์๋ฃ ๋ฑ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃจ๋ ์๋น์ค๋ผ๋ฉด ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ๋ ๊ฒ ํ๋ช ํ ์ ํ์ด์ผ! ๐ฏ
๐ ์ค์ ์ฒดํฌ๋ฆฌ์คํธ
ํ๋ก์ ํธ๋ฅผ ์์ํ๊ฑฐ๋ ๋ฐฐํฌํ๊ธฐ ์ ์ ์ด ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ํ์ธํด๋ด!
โ ๊ฐ๋ ฅํ ๋น๋ฐ๋ฒํธ ์ ์ฑ ์ ์ฉ
โ ๋น๋ฐ๋ฒํธ ํด์ฑ (bcrypt, Argon2)
โ JWT ํ ํฐ ๋ง๋ฃ ์๊ฐ ์ค์
โ MFA(๋ค์ค ์ธ์ฆ) ๊ตฌํ
โ ์ธ์ ๊ด๋ฆฌ ์ ์ ํ ๊ตฌํ
โ OAuth 2.0 ์ ๋๋ก ๊ตฌํ
๋ฐ์ดํฐ ๋ณดํธ
โ HTTPS ํ์ ์ฌ์ฉ
โ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ํธํ
โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ํธํ
โ ๋ฐฑ์ ์ํธํ
โ ๊ฐ์ธ์ ๋ณด ๋ง์คํน
์ ๋ ฅ ๊ฒ์ฆ
โ ๋ชจ๋ ์ฌ์ฉ์ ์ ๋ ฅ ๊ฒ์ฆ
โ SQL Injection ๋ฐฉ์ด
โ XSS ๋ฐฉ์ด
โ CSRF ํ ํฐ ์ฌ์ฉ
โ ํ์ผ ์ ๋ก๋ ๊ฒ์ฆ
์ค์
โ ๋๋ฒ๊ทธ ๋ชจ๋ ๋นํ์ฑํ
โ ์๋ฌ ๋ฉ์์ง ์ต์ํ
โ ๋ณด์ ํค๋ ์ค์
โ CORS ์ ์ ํ ์ค์
โ Rate Limiting ์ ์ฉ
โ ๊ธฐ๋ณธ ๊ณ์ ๋ณ๊ฒฝ/์ญ์
์์กด์ฑ
โ ์ต์ ๋ฒ์ ์ฌ์ฉ
โ ์ทจ์ฝ์ ์ค์บ ์๋ฃ
โ ๋ถํ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๊ฑฐ
โ SRI ํด์ ์ ์ฉ
๋ก๊น & ๋ชจ๋ํฐ๋ง
โ ๋ณด์ ์ด๋ฒคํธ ๋ก๊น
โ ๋ก๊ทธ ๋ณดํธ
โ ์ค์๊ฐ ์๋ฆผ ์ค์
โ ์ด์ ์งํ ํ์ง
์ธํ๋ผ
โ ๋ฐฉํ๋ฒฝ ์ค์
โ ๋คํธ์ํฌ ์ธ๊ทธ๋ฉํ ์ด์
โ ์ต์ ๊ถํ ์์น
โ ์ ๊ธฐ์ ์ธ ๋ฐฑ์
โ ์ธ์๋ํธ ๋์ ๊ณํ
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ฝ๋๋ผ ์ ๋ง ์๊ณ ํ์ด! ๐ ์ฐ๋ฆฌ๊ฐ ํจ๊ป ์ดํด๋ณธ OWASP Top 10์ ์น ๋ณด์์ ๊ธฐ์ด์ด์ ํต์ฌ์ด์ผ. ์ด ๋ด์ฉ๋ค์ ์ ์ดํดํ๊ณ ์ ์ฉํ๋ค๋ฉด, ํจ์ฌ ์์ ํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค ์ ์์ ๊ฑฐ์ผ! ๐ก๏ธ
2. Cryptographic Failures - HTTPS ํ์, ๋น๋ฐ๋ฒํธ๋ ํด์ฑ!
3. Injection - Prepared Statement ์ฌ์ฉ!
4. Insecure Design - ์ค๊ณ ๋จ๊ณ๋ถํฐ ๋ณด์ ๊ณ ๋ ค!
5. Security Misconfiguration - ๊ธฐ๋ณธ ์ค์ ๊ทธ๋๋ก ์ฐ์ง ๋ง!
6. Vulnerable Components - ์์กด์ฑ ์ ๊ธฐ์ ์ผ๋ก ์ ๋ฐ์ดํธ!
7. Authentication Failures - ๊ฐ๋ ฅํ ์ธ์ฆ, MFA ๊ถ์ฅ!
8. Data Integrity Failures - ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ํ์!
9. Logging Failures - ๋ก๊ทธ ๋จ๊ธฐ๊ณ ๋ชจ๋ํฐ๋ง!
10. SSRF - URL ๊ฒ์ฆํ๊ณ ๋ด๋ถ IP ์ฐจ๋จ!
๋ณด์์ ํ ๋ฒ ์ค์ ํ๊ณ ๋๋๋ ๊ฒ ์๋์ผ. ์ง์์ ์ผ๋ก ๊ด์ฌ์ ๊ฐ์ง๊ณ , ์ต์ ํธ๋ ๋๋ฅผ ๋ฐ๋ผ๊ฐ๋ฉฐ, ์ ๊ธฐ์ ์ผ๋ก ์ ๊ฒํด์ผ ํด. ๋ง์น ๊ฑด๊ฐ ๊ด๋ฆฌ์ฒ๋ผ ๋ง์ด์ผ! ๐ช
โข ์๋ฒฝํ ๋ณด์์ ์์ด. ํ์ง๋ง ๊ณ์ ๊ฐ์ ํ ์๋ ์์ง!
โข ์ค์๋ ๋๊ตฌ๋ ํด. ์ค์ํ ๊ฑด ๋นจ๋ฆฌ ๋ฐ๊ฒฌํ๊ณ ๊ณ ์น๋ ๊ฑฐ์ผ.
โข ์ปค๋ฎค๋ํฐ์ ์ํตํด. ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค์ ๊ฒฝํ์์ ๋ฐฐ์ธ ๊ฒ ๋ง์.
โข ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์ ํ์ฉํด๋ด!
์น ๋ณด์์ ์ด๋ ต๊ณ ๋ณต์กํ ์ ์์ง๋ง, ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ ์ฉํ๋ค ๋ณด๋ฉด ์ด๋์ ์์ ํ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค๊ณ ์๋ ์์ ์ ๋ฐ๊ฒฌํ๊ฒ ๋ ๊ฑฐ์ผ! ๐
์ด ๊ธ์ด ๋์์ด ๋์๊ธฐ๋ฅผ ๋ฐ๋ผ! ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋ ๊น์ด ์๊ณ ์ถ์ ์ฃผ์ ๊ฐ ์๋ค๋ฉด, ์ธ์ ๋ ์ง ์ฐพ์์. ์ฐ๋ฆฌ ๋ชจ๋ ํจ๊ป ๋ ์์ ํ ์น์ ๋ง๋ค์ด๊ฐ์! ๐
ํดํผ ์ฝ๋ฉ, ๊ทธ๋ฆฌ๊ณ ์คํ ์ด ์ธ์ดํ! ๐โจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.