...
csurf 모듈
csurf 모듈은 CSRF 공격을 방지하는 보안 모듈이다.
csurf 모듈은 CSRF Secret와 CSRF Token을 만들어서 서로 매칭이 되는지 확인하는 기능을 손쉽게 제공하는 식으로, 보통 CSRF Secret는 session/cookie에 저장하고 CSRF Token은 POST body에 저장한다.
따라서 폼 제출을 하면, csurf 모듈은 사용자의 CSRF Secret와 CSRF Token이 일치하는지 검증하여 사용자가 맞는지 확인 하게 된다. (뒤에 코드에서 자세히 설명)
CSRF 란?
CSRF는 사용자가 의도치 않게 공격자가 의도한 행동을 하게 만드는, 쿠키를 통한 인증방식에서 사용되는 공격기법이다.
예를 들어 특정 페이지에 방문할 때 저절로 로그아웃되거나, 게시글이 써지는 현상을 유도하는 것을 말한다. 심지어 은행과 같은 사이트에서는 다른 사람에게 송금하는 행동을 넣는 등 상황에 따라 크게 악용될 수 있는 공격이다.
CSRF 공격 과정
만일 티켓 예약 서비스인 http://ttticketttt.org/ 에서 이용자가 티켓을 구매했다고 가정하자.
만일 이용자의 유저 쿠키가 탈취됬을경우, 공격자는 다음과 같이 이미지 태그안에 공격코드를 작성하여 이용자에게 보낸다.
<!-- 예매한 티켓을 취소하는 GET 요청 url -->
<img src="http://ttticketttt.org/ticket-cancel/?user_id=134">
이용자가 공격용 페이지를 열면, 브라우저는 이미지 파일을 받아오기 위해 공격용 URL을 열게 된다.
이용자는 의도치 않게 실행하는 순간 브라우저에서 요청을 보내서 공격이 실행되어 구매한 티켓이 취소되는 상황을 겪게 된다.
따라서 이 공격을 막으려면 내가 한 행동(티켓 구매)이 내가 한 것이 맞다는 점을 인증해야 한다. 이때 CSRF 토큰이 사용되며, csurf 모듈은 이 토큰을 쉽게 발급하거나 검증할 수 있도록 도와 준다.
csurf 모듈 사용법
> npm install csurf
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const app = express();
const csrfProtection = csrf({ cookie: true }); // 쿠키에 csrf Secret을 저장. 만일 세션에 저장하고 싶다면 { cookie: false }로
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session(sessionOption));
/* 쿠키파서나 익스프레스-세션 미들웨어보다 아래에 위치해야 한다. 그리고 csrfProtection 미들웨어를 라우터에 장착 */
app.get('/purchase', csrfProtection, (req, res) => {
// 만일 /puchase 페이지로 접속(get) 하면 req.csrfToken()를 실행하여 템플릿 엔진(pug, nunjucks, ejs)에 csrf 토큰을 생성해서 저장 전달
res.render('index', { csrfToken: req.csrfToken() });
});
app.post('/purchase', csrfProtection, (req, res) => {
// /puchase 페이지에서 폼 submit이 오면(post) 폼 태그 안에 있던 csrf 토큰 값을 csrfProtection 미들웨어에서 검증하고 next()
res.send(req.body.text);
});
csurf는 익스프레스의 미들웨어 형식으로 동작하며, 위에서 cookie를 사용하는 것으로 옵션을 설정했으므로, 프로젝트에 cookie-parser 패키지도 연결되어 있어야 하고 위치도 쿠키파서 미들웨어보다 밑에 위치해 있어야 한다.
csurf 동작 과정 (form)
1. 이용자는 티켓을 구매하기 위해 https://ttticketttt.org/purchase 로 접속을 한다.
2. 서버에서 라우터에 get 요청이 오면, 페이지와 CSRF 토큰을 클라이언트에 렌더링하여 제공한다.
토큰 정보는 req.csrfToken() 으로 가져온다.
app.get('/purchase', csrfProtection, (req, res) => {
res.render('index', { csrfToken: req.csrfToken() });
});
[ index.pug ]
3. 그렇게 홈페이지에 접속(get) 해 csrf 토큰을 얻게되고 클라이언트 폼태그에 토큰 정보를 저장하게 된다.
[ index.html ]
<form action="/" method="POST">
<input type="hidden" name="_csrf" value="xe1t9is6-Q1bcuuJ8G5rdTXWCRqzkSat7FUI">
<input type="text" name="text" value="">
<input type="submit" name="submit">
</form>
이때 토큰 정보를 저장한 폼태그의 name은 "_csrf" 이어야 한다.
4. 이제 정보를 작성하고 POST Submit을 하게 되면, input 태그의 name인 _csrf 의 value(토큰)이 서버 라우터로 가, csrfProtection 미들웨어에서 검증하고, 사용자가 일치하면 next를 하게 된다.
app.post('/purchase', csrfProtection, (req, res) => {
res.send(req.body.text);
});
..위의 과정을 정리하자면 다음과 같이 된다.
- 브라우저의 get 요청시, 서버에서 랜덤값 csrf token과 csrf secret을 클라이언트에 보낸다.
- secret → cookie or session
- token → form/request parameter/request header
- 브라우저에서 post 요청시 header에 csrf token을 담고 csrf secret이 담긴 cookie를 동봉하면, 서버에서 decode한 csrf token이 쿠키의 csrf secret와 일치하는지 확인한다.
- 이 값이 일치할 때만 request를 수행한다.
csurf 동작 과정 (Ajax)
폼 태그가 아닌 ajax로 요청할때는 csrf 토큰을 저장할 곳을 폼이 아닌 보통 meta 태그에 저장하는 경우가 많다.
<meta name="csrf-token" content="xe1t9is6-Q1bcuuJ8G5rdTXWCRqzkSat7FUI">
그리고 ajax 요청할 때는 다음과 같이 CSRF-Token헤더에 토큰을 넣는다.
let token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
fetch('/process', {
credentials: 'same-origin',
headers: {
'CSRF-Token': token // fetch 헤더에 'CSRF-Token'을 명시한다
},
method: 'POST',
body: {
favoriteColor: 'blue' // body 데이터
}
})
토큰 에러 날 경우
만일 csrf 토큰을 인증하는데 실패 하였을 경우 따로 에러 처리가 필요할때 다음과 같이 사용할 수 있다.
만일 토큰 인증에 실패하면 에러처리 미들웨어로 점프되며 err.code === 'EBADCSRFTOKEN' 객체가 담기게 된다.
이를 이용해 특단의 처리를 하면 된다.
// ...
// error handler
app.use(function (err, req, res, next) {
if (err.code !== 'EBADCSRFTOKEN')
return next(err) // 만일 토큰 에러가 아닌 다른 에러일경우 다른 에러처리 미들웨어로 보내고
// CSRF token errors 라면 다음과 같이 처리한다.
res.status(403)
res.send('form tampered with')
})
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.