교차 사이트 요청 위조(CSRF) - 보호 미들웨어 누락

Cross-Site Request Forgery (CSRF)

설명

CSRF는 사용자가 로그인된 세션을 악용해, 사용자의 의사와 무관하게 상태 변경 요청(POST/PUT/PATCH/DELETE 등)이 전송되는 취약점입니다. Express에서 쿠키/세션 기반 인증을 쓰면서 적절한 CSRF 방어를 적용하지 않으면, 공격자가 사용자를 악성 페이지로 유도해 자동으로 폼 제출이나 AJAX 요청을 발생시켜 계정 변경, 결제, 설정 수정 같은 행위를 사용자의 세션 권한으로 수행하게 만들 수 있습니다.

잠재적 영향

  • 무단 상태 변경: 공격자가 피해자 세션으로 이메일 변경, 비밀번호 변경, 송금 등 중요한 동작을 실행
  • 데이터 무결성/기밀성 훼손: 계정 정보나 설정이 의도치 않게 변경되거나 민감 정보 노출 유발
  • 금전적/업무 피해: 결제 승인, 주문 생성, 권한 변경 등으로 직접적인 비즈니스 손실 발생

해결 방법

  • CSRF 토큰 검증 적용: 쿠키/세션 기반 인증을 사용한다면 사용 전 유지보수 상태를 확인한 CSRF 미들웨어 또는 프레임워크 내장 보호 기능을 전역(app.use) 혹은 라우트별로 적용하세요.
  • 상태 변경 라우트 보호: POST/PUT/PATCH/DELETE 모든 라우트에 CSRF 검증을 적용하세요.
  • 토큰 처리: 서버에서 생성한 CSRF 토큰을 폼(hidden input)이나 AJAX 헤더(X-CSRF-Token 등)로 전송하고 서버에서 검증하세요.
  • GET은 안전하게: GET 요청으로 상태를 변경하지 마세요.
  • SameSite=strict/lax 쿠키 설정, Referer/Origin 검증(민감 엔드포인트), 필요 시 CAPTCHA/재인증을 추가로 적용하세요.

예시

변경 전

javascript
const express = require("express");
const session = require("express-session");

const app = express();
// 애플리케이션은 직접 공개하지 않고 신뢰하는 단일 TLS 프록시 뒤에서만 실행
app.set("trust proxy", 1);
app.use(express.urlencoded({ extended: false }));
const sessionSecret = process.env.SESSION_SECRET;
if (!sessionSecret || sessionSecret.length < 32) {
  throw new Error("SESSION_SECRET must contain at least 32 characters");
}
app.use(
  session({
    secret: sessionSecret,
    resave: false,
    saveUninitialized: false,
    cookie: { httpOnly: true, secure: true, sameSite: "lax" },
  })
);

// 취약: 세션을 쓰지만 CSRF 보호 미들웨어 없음
app.post("/account/email", (req, res) => {
  const userId = req.session.userId;
  const newEmail = req.body.email;
  // ... DB에 이메일 업데이트 수행
  res.send("updated");
});

app.listen(3000);

변경 후

javascript
const express = require("express");
const session = require("express-session");
const { csrfSync } = require("csrf-sync");

const { csrfSynchronisedProtection, generateToken } = csrfSync({
  getTokenFromRequest(req) {
    if (req.is("application/x-www-form-urlencoded")) {
      return req.body.csrfToken;
    }
    return req.headers["x-csrf-token"];
  },
});

const app = express();
// 애플리케이션은 직접 공개하지 않고 신뢰하는 단일 TLS 프록시 뒤에서만 실행
app.set("trust proxy", 1);
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
const sessionSecret = process.env.SESSION_SECRET;
if (!sessionSecret || sessionSecret.length < 32) {
  throw new Error("SESSION_SECRET must contain at least 32 characters");
}
app.use(
  session({
    secret: sessionSecret,
    resave: false,
    saveUninitialized: false,
    cookie: { httpOnly: true, secure: true, sameSite: "lax" },
  })
);

// 전역 적용: 모든 상태 변경 라우트에 CSRF 토큰 검증이 수행됨
app.use(csrfSynchronisedProtection);

// 폼에서는 `csrfToken` hidden 필드, JSON 요청에서는 `x-csrf-token` 헤더로 전송
app.get("/account/email", (req, res) => {
  res.json({ csrfToken: generateToken(req) });
});

// 안전: 토큰이 없는 요청은 거부됨
app.post("/account/email", (req, res) => {
  const userId = req.session.userId;
  const newEmail = req.body.email;
  // ... DB 업데이트
  res.send("updated");
});

app.listen(3000);

설명:

  • 변경 전: 신뢰하는 TLS 프록시와 환경의 충분히 긴 세션 비밀, 안전한 쿠키 속성을 사용하더라도 CSRF 토큰 검증이 없으면 동일 사이트 공격 등에서 상태 변경 요청을 위조할 수 있습니다.
  • 변경 후: 같은 프록시·세션 설정에 프로젝트에서 채택한 CSRF 미들웨어를 전역 적용해 서버가 발급한 토큰을 검증합니다. 이 예시는 신뢰하는 단일 TLS 프록시 뒤에서 실행하는 구성을 전제로 합니다. 유효한 CSRF 토큰이 없는 상태 변경 요청은 거부됩니다.

참조