사용자 입력 기반 보안 검사 우회

Reliance on Untrusted Inputs in a Security Decision

설명

애플리케이션이 인증/권한을 사용자가 제어할 수 있는 입력값(쿠키, 헤더, 쿼리, 라우트 파라미터 등)에 의존할 때 발생합니다. 특히 서로 다른 사용자 입력끼리 비교해 로그인 여부나 접근 권한을 판단하면, 공격자가 해당 값을 임의로 조작해 보호된 기능이나 데이터에 접근할 수 있습니다. 예를 들어 role=admin 쿠키를 임의로 만들어 보내거나, 쿠키의 userId와 URL의 userId를 맞춰 보내는 식으로 인증을 우회할 수 있습니다.

잠재적 영향

  • 인증 우회: 공격자가 로그인하지 않았거나 다른 사용자임에도 합법적인 사용자로 가장해 접근할 수 있습니다.
  • 권한 상승/수평 이동: role, perm 같은 값을 조작하여 관리자 권한 획득 또는 다른 사용자의 리소스에 접근할 수 있습니다.
  • 민감정보 유출: 개인 정보, 결제 정보, 내부 설정 등 보호된 데이터가 노출될 수 있습니다.
  • 중요 기능 오남용: 관리자 전용 기능(설정 변경, 사용자 관리 등)을 무단 실행해 시스템을 손상시킬 수 있습니다.

해결 방법

  • 서버가 제어하는 신뢰 가능한 값으로만 보안 결정을 하세요: 서버 세션(req.session), 서명된 쿠키(req.signedCookies), 검증된 JWT의 클레임, DB 조회 결과 등을 사용하세요.
  • 사용자 입력 간 교차 비교 금지: req.cookies.userId 와 req.params.userId 같은 두 개의 사용자 입력끼리 비교해 인증/권한을 판단하지 마세요.
  • 권한은 서버 기준으로 결정: userId로 DB에서 사용자/권한을 조회한 뒤, 서버가 정의한 권한 정책에 따라 접근을 허용하세요.
  • 서명/검증 필수: 쿠키는 서명(signed) 또는 서버 세션을 사용하고, JWT는 서명 검증을 반드시 수행하세요. 단순한 평문 쿠키/파라미터를 신뢰하지 마세요.
  • 공통 미들웨어 도입: 인증(Auth)과 권한(Authorization) 검사를 미들웨어로 중앙화하여 모든 민감 라우트에 일관되게 적용하세요.
  • 최소권한 원칙: 기능별로 필요한 최소 권한만 허용하고, 허용 목록(allowlist) 기반으로 확인하세요.

예시

변경 전

javascript
const express = require("express");
const cookieParser = require("cookie-parser");
const app = express();
app.use(cookieParser()); // 서명 없음: 사용자가 쿠키 값을 임의로 변경 가능

// BAD: 사용자 제어 입력(쿠키/쿼리)로 보안 결정을 수행
app.get("/admin", (req, res) => {
  // role과 user는 모두 클라이언트가 임의로 보낼 수 있음
  if (req.cookies.role === "admin" && req.query.user === req.cookies.user) {
    return res.send("admin page");
  }
  return res.status(403).send("forbidden");
});

변경 후

COOKIE_SECRET에는 충분한 엔트로피를 가진 비밀값을 안전하게 주입하세요. 실제 로그인에서 발급한 쿠키를 검증하는 발췌 예시이며, 쿠키 발급과 실제 DB 연동은 생략했습니다.

javascript
const express = require("express");
const cookieParser = require("cookie-parser");
const app = express();
const cookieSecret = process.env.COOKIE_SECRET;
if (!cookieSecret || cookieSecret.length < 32) {
  throw new Error("COOKIE_SECRET must contain at least 32 characters");
}
app.use(cookieParser(cookieSecret)); // 서명된 쿠키 활성화

// 예시: DB에서 사용자 정보를 조회하는 함수(가짜 구현)
async function findUserById(id) {
  // 실제 코드에서는 DB 조회 수행
  const mock = { id: "u123", role: "admin" };
  return id === "u123" ? mock : null;
}

// GOOD: 서버가 신뢰하는 값(서명된 쿠키 + DB 조회)으로 보안 결정
app.get("/admin", async (req, res) => {
  const userId = req.signedCookies.userId; // 서명 검증된 식별자
  if (!userId) return res.status(401).send("login required");

  const user = await findUserById(userId); // 서버 측 DB 기준으로 권한 확인
  if (!user || user.role !== "admin") return res.status(403).send("forbidden");

  return res.send("admin page");
});

설명:

  • 변경 전: 쿠키(req.cookies.role, req.cookies.user)와 쿼리(req.query.user)는 모두 클라이언트가 조작할 수 있는 값입니다. 이 값들끼리 비교해 권한을 판단하면, 공격자는 role=admin 같은 쿠키를 임의로 설정하거나 쿼리 파라미터를 맞춰 보내 권한을 쉽게 우회할 수 있습니다.
  • 변경 후: 서명된 쿠키(req.signedCookies.userId)로 서버가 값의 무결성을 검증하고, 해당 userId로 DB에서 사용자와 권한을 조회해 서버 기준으로 판단합니다. 사용자가 보낸 평문 입력끼리 비교하지 않으므로 값 조작에 의한 인증/권한 우회가 차단됩니다.

참조