설명
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 토큰이 없는 상태 변경 요청은 거부됩니다.