안녕하세요! 웹 개발자 여러분. 💻
오늘은 Node.js와 Express.js로 웹 애플리케이션을 구축할 때 정말 정말 중요한, 하지만 자칫 놓치기 쉬운 보안 설정에 대해 이야기해보려고 합니다. 바로 CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 공격을 막는 방법에 대한 것인데요.
아래와 같은 코드를 미들웨어 설정 부분에서 보신 적이 있으신가요?
// Enable Express csrf protection
app.use(csrf());
// Make csrf token available in templates
app.use((req, res, next) => {
res.locals.csrftoken = req.csrfToken();
next();
});
이 두 덩어리의 코드가 우리 서버를 어떻게 지켜주는지, 그 원리를 쉽고 상세하게 파헤쳐 보겠습니다!

🤔 잠깐, CSRF 공격이 대체 뭔가요?
코드를 살펴보기 전에, 우리가 막으려는 적, CSRF가 무엇인지 간단히 알아볼게요.
CSRF는 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행동을 하게 만드는 공격입니다.
예를 들어볼까요?
- 사용자 👤는 my-bank.com에 로그인한 상태입니다. 브라우저에는 my-bank.com의 인증 정보(쿠키)가 남아있죠.
- 이때 사용자가 악성 이메일이나 게시글에 포함된 링크 evil-site.com을 클릭합니다. 😈
- evil-site.com 페이지에는 눈에 보이지 않는 코드가 숨어있습니다. 예를 들어, my-bank.com/transfer?to=hacker&amount=1000 와 같은 요청을 자동으로 보내는 이미지 태그나 폼(form)이 숨겨져 있을 수 있죠.
- 사용자의 브라우저는 이 요청을 보내면서, my-bank.com의 인증 쿠키를 자동으로 함께 실어 보냅니다. 🍪
- 은행 서버 🏦 입장에서는? 정상적인 인증 쿠키가 포함된 요청이 왔으니, "아, 정상 사용자가 송금을 요청하는구나!"라고 착각하고 공격자의 계좌로 돈을 보내버립니다.
끔찍하죠? 사용자는 그저 링크 하나를 클릭했을 뿐인데, 자신도 모르는 사이에 중요한 정보가 변경되거나 금전적 피해를 볼 수 있는 매우 위험한 공격입니다.
🔐 1단계: CSRF 방어막 활성화! app.use(csrf());
이제 첫 번째 코드를 살펴봅시다.
// Enable Express csrf protection
app.use(csrf());
이 한 줄의 코드는 우리 Express 서버에 강력한 방어막을 설치하는 것과 같습니다. 이 코드가 추가되면 서버는 다음과 같은 일을 시작합니다.
- 비밀 열쇠(CSRF 토큰) 발급 🔑: 서버는 각 사용자 세션마다 고유하고 예측 불가능한 문자열인 'CSRF 토큰'을 생성합니다. 이 토큰은 마치 일회용 비밀번호와 같습니다.
- 요청 감시 및 검증 🧐: 이제부터 서버는 상태를 변경하는 모든 요청(POST, PUT, DELETE 등)에 대해 이 비밀 열쇠(CSRF 토큰)가 올바르게 포함되어 있는지 검사합니다.
- 요청에 토큰이 없거나, 서버가 발급한 토큰과 일치하지 않으면? ➡️ 차단! ❌ 서버는 해당 요청을 위험한 것으로 간주하고 에러를 발생시키며 요청을 거부합니다.
이제 공격자 😈가 evil-site.com에서 몰래 요청을 보내도, 이 비밀 열쇠(CSRF 토큰)를 모르기 때문에 서버는 요청을 차단하게 됩니다. 이로써 CSRF 공격의 대부분을 막을 수 있습니다!
💌 2단계: 비밀 열쇠를 프론트엔드로 안전하게 전달하기
자, 이제 서버는 비밀 열쇠를 만들고 검증할 준비가 되었습니다. 하지만 한 가지 문제가 남았습니다.
"사용자가 정상적으로 보내는 요청에는 어떻게 이 비밀 열쇠를 포함시키죠?" 🤔
서버만 비밀 열쇠를 알고 있으면 안 되겠죠. 사용자 측(프론트엔드)에서도 이 열쇠를 받아서, 요청을 보낼 때 함께 보내줘야 합니다. 바로 이 역할을 두 번째 코드가 담당합니다.
// Make csrf token available in templates
app.use((req, res, next) => {
res.locals.csrftoken = req.csrfToken();
next();
});
이 코드는 모든 요청에 대해 순차적으로 다음 작업을 수행합니다.
- req.csrfToken() 호출: csrf() 미들웨어가 요청(req) 객체에 만들어준 csrfToken() 함수를 호출합니다. 이 함수는 현재 세션에 대한 비밀 열쇠(CSRF 토큰)를 반환합니다.
- res.locals에 저장: res.locals는 해당 요청을 처리하는 동안에만 유효한 데이터를 담아두는 특별한 주머니입니다. 여기에 csrftoken이라는 이름으로 방금 얻은 비밀 열쇠를 저장합니다.
- 템플릿에서 사용 가능: res.locals에 저장된 값은 EJS, Pug, Nunjucks 같은 템플릿 엔진에서 직접 변수처럼 사용할 수 있습니다. 즉, HTML을 렌더링할 때 이 비밀 열쇠 값을 손쉽게 심어둘 수 있게 된 것이죠!
✨ 최종 단계: 프론트엔드에서 비밀 열쇠 사용하기
이제 모든 준비가 끝났습니다. 프론트엔드 템플릿(예: form.ejs 파일)에서는 다음과 같이 res.locals에 저장해 둔 토큰을 사용할 수 있습니다.
<form action="/update-profile" method="POST">
<input type="hidden" name="_csrf" value="<%= csrftoken %>">
<div>
<label for="username">이름:</label>
<input type="text" id="username" name="username">
</div>
<button type="submit">프로필 업데이트</button>
</form>
이렇게 하면 사용자가 '프로필 업데이트' 버튼을 눌러 폼을 전송할 때, 눈에 보이지 않는 _csrf 필드에 우리의 비밀 열쇠(CSRF 토큰)가 담겨 서버로 전송됩니다. 서버는 이 토큰을 보고 "아, 내가 발급한 비밀 열쇠를 가진 정상적인 요청이구나!"라고 판단하고 요청을 안전하게 처리합니다. ✅
정리하며
오늘 살펴본 두 줄의 코드는 다음과 같은 완벽한 CSRF 방어 체계를 구축합니다.
- app.use(csrf());: CSRF 토큰을 생성하고, 들어오는 모든 요청을 감시하며 토큰을 검증하는 방어막을 활성화합니다.
- app.use((req, res, next) => { ... });: 방어에 필요한 CSRF 토큰을 프론트엔드 템플릿으로 안전하게 전달하는 역할을 합니다.
- 프론트엔드: 전달받은 토큰을 폼(form)의 숨겨진 필드에 포함시켜, 서버에 "나는 정상적인 사용자입니다" 라는 증표를 함께 보냅니다.
웹 보안은 아무리 강조해도 지나치지 않습니다. 특히 사용자의 중요한 정보를 다루는 서비스라면 CSRF 방어는 선택이 아닌 필수입니다. 이 코드를 통해 여러분의 소중한 서비스를 더욱 안전하게 만드시길 바랍니다.
행복한 코딩하세요! 🚀
태그: Nodejs, Express, CSRF, Security, WebDev, csurf, 웹보안, 노드제이에스
'일반IT > IT보안' 카테고리의 다른 글
| [IT 상식] PCI DSS? GDPR? 헷갈리는 보안 인증, 5분 만에 완벽 정리! 💳 (1) | 2025.10.08 |
|---|---|
| 다운로드 파일, 정말 믿어도 될까요? GPG 서명으로 안전하게 검증하기 🛡️ (0) | 2025.10.07 |
| ⛑️ 내 Express 서버에 안전모 씌우기: Helmet 미들웨어 A to Z (0) | 2025.10.05 |
| 🛡️ OffSec 자격증의 모든 것: 가격, 시험 방식 (객관식? 놉!) 총정리 (1) | 2025.10.04 |
| 🚨 "연결이 안전하지 않습니다" 경고의 진실: Burp Suite와 HTTPS의 만남 (0) | 2025.10.04 |