[Nest.js/CSRF] 크로스 사이트 요청 위조
CSRF(Cross Site Request Forgery Attack, 크로스 사이트 요청 위조 공격)
- 인증된 사용자가 웹 애플리케이션에 특정 요청을 보내도록 유도공격하는 행위를 의미한다.
- 쉽게 말하자면, 공격자의 요청이 사용자의 요청인 것처럼 속이는 공격방식 을 의미한다.
CSRF 공격을 어떤식으로 하는가?
공격자가 사용자인것처럼 위장하면서 요청을 보내도록 하는 것이므로
제품구입, 계정설정, 기록삭제, 비밀번호 변경, 문자 전송과 같은 데이터 값을 변경하는 요청일 때 공격한다.
- 이메일이나 웹사이트에 하이퍼링크를 심어서 요청을 보내고 하이퍼링크를 클릭하게 되면 요청이 자동으로 전송된다.
- 사용자 몰래 자금을 전송
- 이메일 주소와 비밀번호를 변경하는 무단행위
CSRF를 어떻게 방지할까?
- CSRF 토큰을 통해 요청이 사용자가 전송한 것이 맞는 확인하거나 재인증을 요구하는 조치를 취하고 있다.
- 사용자 본인의 정보를 안전하게 보관하는게 제일 중요하다.
- 사용하지 않는 웹 애플리케이션 로그아웃하기
- 로그인 정보 안전하게 보관하기
- 여러사이트에서 동일한 비밀번호를 사용하거나 유추하기 쉬운 비밀번호를 사용하면 위조공격에 노출될 가능성이 높다.
- 브라우저에 비밀번호 저장하지 않기
- CSRF는 사용자의 브라우저를 대상으로 하기때문에, 브라우저에 비밀번호를 저장하는 경우 비밀번호가 유출되어 공격대상이 될 수 있다.
- 불편하더라도 브라우저에는 비밀번호를 저장하지 않는 것을 추천한다.
- 여러 웹사이트 동시에 사용하지 않기
- VPN 사용하기
CSRF와 XSS(Cross Site Scripting)의 공통점과 차이점
- 공통점: 사용자의 브라우저를 대상으로 한다.
- 차이점
- 1) CSRF는 사용자의 인증된 세션을 악용하는 공격방식이라면 XSS는 인증된 세션없이도 공격을 진행이 가능하다.
- 2) XSS는 사용자에서 스크립트가 실행되지만 CSRF의 요청위조는 서버에서 스크립트가 실행된다.
- 3) XSS는 사용자의 PC에서 스크립트를 실행해 사용자의 정보를 탈취하는 것을 목적으로 하고
- CSRF는 요청을 위조함으로써 사용자 몰래 송금과 제품구입등 특정 행위를 수행하는 것을 목적으로 한다.
[참고] Understanding CSRF
[참고] Difference between XSS and CSRF attacks
CSRF 공격방식을 어떻게 대비 할까?
1. 서버에서 csrf 토큰을 생성한 뒤에 쿠키에 csrf토큰을 클라이언트에게 보내준다.
2. 클라이언트가 다시 요청을 하게되면, 서버는 csrf 토큰을 검증한다.
csrf토큰을 갖지않고 요청한다면 서버는 응답을 거부한다.
자료를 좀더 조사를 해봤더니
폼요청과 같은 post/put/delete/patch와 같은 데이터의 변경을 요구하는 api요청시
클라이언트가 서버에서 요청할 때, 무조건 프론트단에서는 csrf 토큰값을 같이 전달을 해야하며
axios나 fetch로 서버에게 요청을 한다면, 토큰과 같이 { withCredential : true } 옵션도 같이 부여하는 것이 좋다.
html코드에서 form요청일 경우에는 _csrf 이름으로 쿠키에 저장된 csrf토큰값을 전달하면 된다.
<form>
<input type='hidden' name='_csrf' value='<%= csrfToken %>'>
<label for='name'> Name:</label>
<input type='text' name='name'>
<button type='submit'> Update </button>
</form>
[ ncsrf 는 시도했지만, node 16 version 이상일때 충돌이 생겨서 보류하고 다른방법으로 대체했습니다. ]
- 필자 사용 Node version: v18.5.0
- 환경: mac OS
관련 패키지 설치하기
$ npm install ncsrf --save
아래와 같은 이미지로 npm 으로 ncsrf가 설치가 안되는 경우
(해결방안1) 옵션을 부여하여 강제설치
원인은 패키지에서 요구하는 dependency가 일치하지 않아서 충돌이 일어난 것이다.
에러로그를 읽어보면 npm ERR! Conflicting peer dependency: @nestjs/common@8.4.7 으로 되어있고
에러로그가 해결방안도 알려준다.
"Fix the upstream dependency conflict, or retry. this command with --force, or --legacy-peer-deps"
--force 옵션이나 --legecy-peer-deps 옵션을 줘서 패키지 설치하게되면 에러없이 설치가 정상적으로 된다.
# 1. --force 옵션을 사용했을 때 (충돌우회)
# package-lock.json에 다른버젼의 의존패키지를 설치
$ npm install ncsrf --force --save
# 2. --legacy-peer-deps 옵션을 사용했을 때 (충돌무시)
$ npm instlal helmet --legacy-peer-deps
(해결방안2) nodejs version 을 다운그레이드 한다.
$ brew install node@16
다운그레이드 버젼의 node를 연결시킨다.
$ brew link node@16
node 16ver 을 다운그레이드하려고 brew를 사용하다가 발생한 문제이다
"You have not agreed to the Xcode license."
검색을 해보니 라이센스 미동의로 인한 문제이다. 아래 명령어를 실행시키고 다시 해보자.
$ sudo xcodebuild -license accept
공식다큐먼트의 예제에서도 @Csrf 라는 데코레이터로 csrf토큰이 존재하는지를 확인을 할 수 있다.
쿠키에서 _csrf 에 해당하는 값을 파싱하는 로직이 필요하다.
ncsrf는 nodejs 16version 이상의 환경에도 빌드가 안되는 신기한 문제를 겪었다.
왜냐하면 github action에서의 빌드도 실패를 했기 때문이다.
실패 이유를 보니, node 16버젼에서 npm ci 명령어를 실행하는데 충돌이 일어났다.
0s
1s
Run npm ci
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR!
npm ERR! While resolving: ncsrf@1.0.3
npm ERR! Found: @nestjs/common@9.2.0
npm ERR! node_modules/@nestjs/common
npm ERR! @nestjs/common@"^9.0.0" from the root project
npm ERR! peer @nestjs/common@"^8.2.3 || ^9.0.0" from @nestjs/apollo@10.1.6
npm ERR! node_modules/@nestjs/apollo
npm ERR! @nestjs/apollo@"^10.1.6" from the root project
npm ERR! 11 more (@nestjs/config, @nestjs/core, @nestjs/graphql, ...)
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer @nestjs/common@"^8.4.4" from ncsrf@1.0.3
npm ERR! node_modules/ncsrf
npm ERR! ncsrf@"^1.0.3" from the root project
npm ERR!
npm ERR! Conflicting peer dependency: @nestjs/common@8.4.7
npm ERR! node_modules/@nestjs/common
npm ERR! peer @nestjs/common@"^8.4.4" from ncsrf@1.0.3
npm ERR! node_modules/ncsrf
npm ERR! ncsrf@"^1.0.3" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See /home/runner/.npm/eresolve-report.txt for a full report.
npm ERR! A complete log of this run can be found in:
npm ERR! /home/runner/.npm/_logs/2023-01-26T06_01_57_011Z-debug-0.log
Error: Process completed with exit code 1.
nestjs 공식문서에는 csurf 도 있다. ncsrf와 달리 helmet 처럼 사용하기 편하다.
그래서 ncsrf 라이브러리 사용하는 대신에 csurf와 helmet을 사용하는 방안으로 했다.
ncsrf를 시도하다가 패키지 설치가 안되는 문제를 겪었는데, 공유하기에 좋은 주제인거같아서 잠시나마 소개를 하기로 했다 :)
npm 의 --legacy-peer-deps 와 --force 옵션의 차이점
두 옵션의 차이점이 궁금해졌다.
--legacy-peer-deps 옵션
- 충돌 무시
- peer-dependency 를 맞추지 않고 설치한다.
- 즉, package-lock.json에 다른 버젼의 의존패키지를 설치하지 않는다.
--force 옵션
- 충돌 우회
- package-lock.json 에 의존버젼들을 추가
먼저 --force 옵션으로 인해 의존버젼을 추가하여 충돌을 우회하고, 이후에도 되지 않는다면
--legacy-peer-deps 로 충돌을 무시하여 오류없이 설치해서 실행한다.
다만, 이후에도 새로운 패키지를 설치할 때도 매번 옵션을 붙여가면서 해야되는 번거로움이 있다.
> stackoverflow에서 공유된 질문과 답변
1. 더이상 버젼업이 되지 않지만, 그나마 csrf 대비하는 모듈중 많이 사용한 csurf 를 사용했다.
$ npm install csurf
2. main.ts 에 쿠키파서 밑에 정의한다.
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';
import helmet from 'helmet';
import * as csurf from 'csurf';
async function bootstrap() {
// cors(cross-origin-resource-sharing) 적용
// 다른도메인에서 리소스 요청을 허용하게끔 한다.
const app = await NestFactory.create(AppModule, { cors: true });
...
// 쿠키적용
app.use(cookieParser());
// csrf
/*
쿠키에 csrf Secret을 저장. '/user/'이 맨앞에 있는 url 형식을 가진 api이고
메소드가 post/put/delete 인 api를 요청할때 csrf 토큰이 쿠키에 존재하지 않으면 응답을 거부
*/
app.use('/user/', csurf({ cookie: { sameSite: true } }));
// helmet 적용
app.use(helmet());
await app.listen(process.env.SERVER_PORT);
}
bootstrap();
3. csrf 토큰을 요청하는 get방식의 api를 만들기
@ApiTags('Auth')
@UseFilters(HttpExceptionFilter)
@Controller()
export class AuthController {
constructor(
private readonly configService: ConfigService,
private readonly authService: AuthService,
private readonly usersService: UsersService,
) {}
...
// csrf token을 얻는다.
@UseGuards(JwtAuthGuard) // 로그인이 되어있는 상태일 때 csrf 토큰을 발급할 수 있음.
@Get('auth/csrf')
async getCsrfToken(@Req() req, @Res() res: Response) {
res.cookie('XSRF-TOKEN', req.csrfToken(), { httpOnly: true });
return res.json({});
}
// csrf 토큰이 필요한 api
@UseGuards(JwtAuthGuard)
@Patch('user/:id')
async updateUserInfo(@Req() req) {
return { message: 'protected' };
}
}
- cookie로 csrf토큰을 저장할 때는 XSRF-TOKEN 의 키값으로 한다.
- header로 csrf토큰을 저장할때는 X-XSRF-TOKEN 의 키값으로 한다.
[결과]
1) 로그인을 한다.
2) CSRF 토큰 발급
로그인 이후, csrf 토큰을 발급하면, 기본적으로 csurf모듈을 사용함으로써 쿠키로 생성하는 _csrf가 자동적으로 만들어지고
XSRF-TOKEN은 쿠키에서 csrf토큰을 생성방식으로 필자에 의해서 생성한거다.
3) CSRF 토큰 발급 이후, CSRF 토큰이 필요한 API 요청 테스트 (feat. insomnia)
[참고] csrf토큰 구현하기
[참고] postman으로 csrf 토큰 테스트하기
아직 궁금한 점이 있긴한다..
- 해커와 실제 사용자가 동시에 같은계정(사용자계정)으로 접속했을 때 csrf는 진짜 사용자가 누구인지 어떻게 알 수 있을까?
- 해커가 사용자의 아이디와 비밀번호를 알게된다면, 쿠키에는 csrf토큰값이 있을텐데 이게 왜 해커가 사용자인척하는 csrf 공격을 막을 수 있는건지 자료를 찾아봐도 잘모르겠다. 조금 더 찾아보고 분석해볼 필요가 있다 ㅠㅠ
그러나 필자가 보기에는 쿠키에서 값을 찾는다 하더라도 파싱한다하더라도 accessToken과 비슷해보였다.
2단계 인증으로 요청클라이언트가 맞는지 직접 클라이언트에게 확인하는 방식이 더 안전한 거같다.
아래 네이버처럼 새로운 환경에서 로그인했다는 것을 메일알림을 받아서 인증하는거처럼 말이다.
그러면 또 궁금한점이 생겼다. "로그인 한 환경/국적/기기를 어떻게 알 수있는걸까?"
helmet을 사용해도된다.
helmet에도 csrf, xss 등을 대비하는 해킹에 대비할 수 있는 함수들이 있다.
helmet은 node.js 환경에서 많이 사용되는 보안관련 패키지다.
http헤더를 적절히 설정하여 잘 알려진 일부 웹 취약점으로부터 앱을 보호한다.
http 헤더를 설정하는 15개가량의 미들웨어들이 있다.
설치 및 사용방법도 심플하다.
$ npm install helmet --save
주로 main함수에서 app.use(helmet()) 한줄만 입력해도 된다.
https://www.npmjs.com/package/helmet
[참고]
[CSRF]
https://nordvpn.com/ko/blog/csrf/
https://lts0606.tistory.com/627
https://itnext.io/how-to-secure-your-web-applications-part-1-cpas-3-715b72973623
[Access Token의 문제점과 Refresh Token]
https://hudi.blog/refresh-token/
[npm 패키지 설치 오류 해결방안]
https://velog.io/@dev_cecy/npm-install-%EC%98%A4%EB%A5%98-code-ERESOLVE
[csrf 구현]
https://portswigger.net/web-security/csrf/bypassing-token-validation
https://blog.naver.com/PostView.naver?blogId=sssang97&logNo=221942513392
https://github.com/wafflestudio/seminar-2020/issues/108