인증 vs 인가: 누구나 호출할 수 있는 엔드포인트
이 두 단어는 비슷하게 들리고, 진짜 침해는 그 차이 속에 산다.
- **인증(Authentication)**은 *당신은 누구인가?*다 — 로그인, 신원 증명.
- **인가(Authorization)**는 *당신은 무엇을 할 수 있는가?*다 — 이 로그인한 사용자가 이 데이터에 이 동작을 수행해도 되는지.
AI는 인증은 꽤 잘한다; 라이브러리가 대부분을 처리한다. 인가는 AI가 일상적으로 실패하는 곳인데, 인가는 당신 앱의 규칙에 특화되어 있고 AI는 그 규칙을 모르기 때문이다. 교과서적인 재앙:
// VULNERABLE: 로그인했는지는 점검하지만, 당신이 누구인지는 점검하지 않는다
app.get("/admin/export-all-users", requireLogin, (req, res) => {
res.json(db.getAllUsers()); // 로그인한 사용자라면 누구나 이걸 칠 수 있다
});
그 엔드포인트는 "보호"되어 있다 — 로그인해야만 한다. 하지만 30초 전에 가입한 사람을 포함해 어떤 로그인 사용자든 그것을 호출해 모든 사용자의 데이터를 내려받을 수 있다. 인증은 있고, 인가는 없다. 해결책은 동작 그 자체에 권한을 점검하는 것이다:
// SAFE: 이 사용자가 실제로 admin인지 확인한다
app.get("/admin/export-all-users", requireLogin, (req, res) => {
if (!req.user.isAdmin) return res.status(403).send("Forbidden");
res.json(db.getAllUsers());
});
요청이 통과해야 하는 두 개의 게이트를 생각해 보라. 인증은 "로그인했나?"를 묻고, 인가는 "당신이 이것을 해도 되나?"를 묻는다. 침해는 앱이 첫 번째 게이트를 만들고 두 번째를 잊을 때 일어난다:
요청 ──▶ ┌──────────────────┐ ──▶ ┌──────────────────┐ ──▶ 실행
│ 인증(authn) │ │ 인가(authz) │ 됨
│ "로그인됨?" │ │ "이 사용자가 │
│ │ │ 이 작업 가능?" │
└──────────────────┘ └──────────────────┘
✓ 대부분 구현 ✗ 자주 누락
하는 부분 → 바로 유출
예시 — /admin/export-all-users
로그인 사용자 ──▶ [auth ✓] ──▶ [ authz 검사 없음 ] ──▶ 전 사용자 유출
로그인 사용자 ──▶ [auth ✓] ──▶ [ isAdmin? → 403 ] ──▶ 차단됨 ✓
같은 함정이 도처에서 축소판으로 나타난다: 주문 #1234를 반환하면서 그 주문이 요청한 사용자의 것인지 점검하지 않는 엔드포인트. 누구나 의 숫자를 바꿔서 남의 주문을 읽을 수 있다. 규칙은 화려하지 않고 절대적이다: 데이터를 다루는 모든 엔드포인트에서 인가를 점검하고, 클라이언트가 보낸 ID를 신뢰하지 말라. 숨겨진 URL이 숨겨져 있다는 이유로 안전하다고 가정하지 말라 — "아무도 이게 존재하는 줄 모른다"는 보안 통제가 아니다.