OWASP Top 10 (2026): 웹 보안 취약점 총정리
OWASP Top 10 최신 목록을 바탕으로, 각 취약점의 원리와 방어 방법을 코드 예시와 함께 정리했다.
웹 애플리케이션을 만들면서 보안을 나중에 생각하는 경우가 많다. 기능 먼저 만들고 배포하고, 보안은 "나중에 시간 되면" 하겠다고. 그런데 그 "나중"은 보통 보안 사고가 터진 후다.
OWASP(Open Web Application Security Project)는 웹 보안 관련 비영리 재단이다. 이 재단이 주기적으로 발표하는 "Top 10"은 가장 빈번하고 위험한 웹 보안 취약점 10가지를 정리한 목록이다. 웹 개발자라면 최소한 이 목록에 있는 취약점들은 알고 있어야 한다.
A01: Broken Access Control — 접근 제어 실패
1위. 가장 흔하고 가장 위험하다. 사용자가 자신의 권한을 벗어나 다른 사용자의 데이터에 접근하거나 관리자 기능을 사용할 수 있는 경우다.
대표적인 패턴:
// URL에서 사용자 ID를 바꿔보는 것만으로 다른 사용자의 정보가 보인다
GET /api/users/1234/profile → 내 프로필
GET /api/users/1235/profile → 다른 사람의 프로필이 그냥 보인다
이런 걸 IDOR(Insecure Direct Object Reference)이라고 한다. 놀라울 정도로 흔하다. API 개발할 때 "요청한 사용자가 이 데이터에 접근할 권한이 있는가?"를 확인하지 않아서 생긴다.
방어:
// BAD — ID만 보고 데이터를 반환
app.get('/api/orders/:id', async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order);
});
// GOOD — 현재 사용자의 주문인지 확인
app.get('/api/orders/:id', async (req, res) => {
const order = await Order.findById(req.params.id);
if (order.userId !== req.user.id) {
return res.status(403).json({ error: 'Forbidden' });
}
res.json(order);
});
기본 원칙은 deny by default다. 명시적으로 허용하지 않은 건 전부 차단. 허용 목록 방식으로 접근 제어를 설계해야 한다.
A02: Cryptographic Failures — 암호화 실패
민감한 데이터를 적절하게 보호하지 않는 경우. 비밀번호를 평문으로 저장하거나, HTTP로 민감 데이터를 전송하거나, 약한 암호화 알고리즘을 쓰거나.
흔한 실수들:
- 비밀번호를 SHA-256으로 해싱 (솔트 없이, 또는 bcrypt/Argon2 대신)
- API 키를 소스 코드에 하드코딩
- 오래된 TLS 버전(1.0, 1.1) 지원
- 에러 메시지에 스택 트레이스나 DB 정보 노출
// BAD
const hashedPassword = crypto.createHash('sha256').update(password).digest('hex');
// GOOD — bcrypt 사용, 자동으로 솔트 포함
import bcrypt from 'bcrypt';
const hashedPassword = await bcrypt.hash(password, 12);
A03: Injection — 인젝션
사용자 입력이 명령어나 쿼리의 일부로 실행되는 경우. SQL 인젝션이 가장 유명하지만, NoSQL 인젝션, OS 커맨드 인젝션, LDAP 인젝션 등 종류가 다양하다.
클래식한 SQL 인젝션:
// BAD — 사용자 입력을 쿼리에 직접 넣음
const query = `SELECT * FROM users WHERE email = '${email}'`;
// email에 ' OR '1'='1 을 넣으면?
// SELECT * FROM users WHERE email = '' OR '1'='1'
// → 전체 사용자 목록이 반환된다
// GOOD — Parameterized query
const query = 'SELECT * FROM users WHERE email = $1';
const result = await db.query(query, [email]);
ORM을 쓰면 기본적으로 파라미터화된 쿼리를 사용하니까 SQL 인젝션에는 상대적으로 안전하다. 하지만 Raw 쿼리를 직접 작성할 때는 항상 주의해야 한다. Prisma든 TypeORM이든, $queryRaw 같은 메서드를 쓸 때 문자열 보간을 하면 그건 SQL 인젝션 구멍이다.
XSS(Cross-Site Scripting)도 인젝션의 일종이다:
<!-- BAD — 사용자 입력을 HTML에 그대로 삽입 -->
<div>{userInput}</div>
<!-- userInput이 <script>alert('hacked')</script> 라면? -->
<!-- React는 기본적으로 이스케이핑을 해주지만 -->
<!-- dangerouslySetInnerHTML을 쓰면 무방비 -->
<div dangerouslySetInnerHTML={{ __html: userInput }} />
React는 JSX에서 자동으로 이스케이핑을 해주니까 XSS에 비교적 안전하다. 하지만 dangerouslySetInnerHTML을 쓰면 그 보호가 사라진다. 이름에 "dangerously"가 들어간 데는 이유가 있다.
A04: Insecure Design — 불안전한 설계
코드 레벨의 버그가 아니라 설계 자체의 문제. 예를 들어 비밀번호 재설정 질문이 "어머니 성함"인데, 이 정보가 SNS에 공개되어 있다면 설계가 잘못된 거다.
또 다른 예시: 비밀번호 찾기 시 "등록되지 않은 이메일입니다"와 "비밀번호가 틀렸습니다"를 구분해서 알려주면, 공격자가 어떤 이메일이 가입되어 있는지 파악할 수 있다. 응답 메시지를 동일하게 만들어야 한다.
위협 모델링(Threat Modeling)을 설계 단계에서 하라는 이야기다. "이 기능이 악용된다면 어떤 시나리오가 가능한가?"를 미리 생각하는 것.
A05: Security Misconfiguration — 보안 설정 오류
기본 비밀번호 그대로 사용, 불필요한 포트 오픈, 디렉토리 리스팅 활성화, 디버그 모드 프로덕션 배포 등.
자주 보이는 실수들:
- 클라우드 S3 버킷 퍼블릭 설정
.env파일이 웹에서 접근 가능- Spring Boot Actuator 엔드포인트가 인증 없이 노출
- CORS를
*로 설정 - 에러 페이지에서 서버 버전, 프레임워크 정보 노출
// BAD
app.use(cors({ origin: '*' }));
// GOOD
app.use(cors({
origin: ['https://yourdomain.com'],
methods: ['GET', 'POST'],
credentials: true
}));
보안 헤더도 중요하다. Content-Security-Policy, X-Content-Type-Options, Strict-Transport-Security 같은 헤더를 설정해야 한다. Next.js는 next.config.js에서 보안 헤더를 추가할 수 있다.
A06: Vulnerable and Outdated Components — 취약하고 오래된 컴포넌트
npm 패키지 하나가 수백 개의 의존성을 끌고 온다. 그중 하나라도 알려진 취약점이 있으면 문제다. 2021년의 Log4Shell 사태가 대표적 — log4j 하나의 취약점이 전 세계를 뒤흔들었다.
# npm 프로젝트의 취약점 검사
npm audit
# 자동 수정 시도
npm audit fix
# GitHub Dependabot이나 Snyk을 CI에 연동하면
# PR 단위로 취약점을 잡아준다
의존성 업데이트를 미루면 기술 부채가 쌓이고, 어느 순간 한꺼번에 올리려고 하면 호환성 문제가 터진다. 자동화된 의존성 업데이트(Dependabot, Renovate)를 설정해두는 게 낫다.
A07: Identification and Authentication Failures — 인증 실패
약한 비밀번호 허용, 브루트포스 차단 미비, 세션 관리 부실 등.
// 비밀번호 정책 검증 예시
function validatePassword(password: string): boolean {
if (password.length < 12) return false;
if (!/[A-Z]/.test(password)) return false;
if (!/[a-z]/.test(password)) return false;
if (!/[0-9]/.test(password)) return false;
return true;
}
이것만으로는 부족하다. 비밀번호가 유출된 목록에 있는지도 확인해야 한다. Have I Been Pwned API를 활용하면 된다. 그리고 로그인 시도 횟수 제한(Rate Limiting)은 필수다. 5회 실패 시 일시 차단 같은 정책.
세션 관리에서 흔한 실수:
- 로그인 후 세션 ID를 재발급하지 않음 (세션 고정 공격에 취약)
- 로그아웃 시 서버 측 세션을 무효화하지 않음
- JWT 토큰의 만료 시간이 너무 길거나 아예 없음
A08: Software and Data Integrity Failures — 소프트웨어 및 데이터 무결성 실패
CI/CD 파이프라인, 자동 업데이트, 직렬화 과정에서 무결성 검증을 하지 않는 경우. SolarWinds 공격이 이 카테고리의 교과서적 사례다 — 빌드 시스템에 악성 코드를 주입해서 정상적인 업데이트 경로로 악성코드를 배포했다.
// Insecure deserialization 예시 — 사용자 입력을 그대로 역직렬화
// BAD
const userData = JSON.parse(req.body.data);
// 이건 JSON이라 상대적으로 안전하지만
// 진짜 위험한 건 eval이나 Function 생성자 사용
// BAD — 절대 하면 안 됨
const result = eval(userInput);
서명되지 않은 직렬화 데이터를 신뢰하지 말 것. CI/CD 파이프라인의 접근 제어를 강화할 것. 패키지 무결성을 검증할 것(lock 파일 + integrity 해시).
A09: Security Logging and Monitoring Failures — 로깅 및 모니터링 부재
공격이 일어나고 있는데 아무도 모르는 상황. 로그를 남기지 않거나, 남기더라도 아무도 보지 않거나, 알람이 설정되어 있지 않거나.
어떤 이벤트를 로그로 남겨야 하나:
- 로그인 성공/실패 (특히 실패 반복)
- 접근 제어 실패
- 입력 검증 실패
- 관리자 행위 (사용자 권한 변경 등)
// 로그인 실패 기록 예시
logger.warn('Login failed', {
email: maskEmail(email),
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: new Date().toISOString()
});
로그에 비밀번호나 신용카드 번호를 남기면 안 된다. 이건 로깅의 기본인데 의외로 실수가 많다. 민감 정보는 마스킹하거나 아예 기록하지 않아야 한다.
A10: Server-Side Request Forgery (SSRF)
서버가 공격자가 지정한 URL로 요청을 보내게 만드는 공격. 내부 네트워크에서만 접근 가능한 서비스에 서버를 통해 간접 접근하는 거다.
// BAD — 사용자가 제공한 URL로 서버가 요청
app.get('/fetch', async (req, res) => {
const response = await fetch(req.query.url as string);
const data = await response.text();
res.send(data);
});
// url=http://169.254.169.254/latest/meta-data/
// → AWS 메타데이터 서비스에 접근. IAM 자격 증명 탈취 가능
AWS의 인스턴스 메타데이터 서비스(169.254.169.254)가 대표적인 타깃이다. 2019년 Capital One 해킹이 SSRF + 메타데이터 서비스 조합이었다.
방어:
- 사용자 입력 URL을 허용 목록(whitelist)으로 검증
- 내부 IP 대역(10.x, 172.16.x, 192.168.x, 169.254.x)으로의 요청 차단
- AWS IMDSv2 사용 (토큰 기반, SSRF로 접근 어렵게 만듦)
실제로 어디서부터 시작하면 되나
10가지를 한꺼번에 다 적용하려면 부담스럽다. 우선순위를 매기자면:
- 인젝션 방어 — 파라미터화된 쿼리 사용. ORM을 쓰고 있다면 Raw 쿼리 부분만 점검
- 접근 제어 — 모든 API 엔드포인트에 권한 검사가 있는지 확인
- 의존성 감사 —
npm audit을 CI에 포함시키고 Dependabot 활성화 - 보안 헤더 — CSP, HSTS 등 설정
- MFA — 최소한 관리자 계정에는 반드시
완벽한 보안은 없다. 하지만 OWASP Top 10에 있는 기본적인 것들만 챙겨도 대부분의 공격은 막을 수 있다. 이 목록은 "고급 보안 기법"이 아니라 "최소한 이것만은 하자"에 가깝다.