~/VibeHandbook
무료 PDF

챕터 18 · 04

인증 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이 숨겨져 있다는 이유로 안전하다고 가정하지 말라 — "아무도 이게 존재하는 줄 모른다"는 보안 통제가 아니다.

오프라인으로 보고 싶으세요?

책 전체를 PDF나 EPUB으로 무료로 내려받으세요.