CSRF(Cross-Site Request Fogery)
피해자가 서버로 공격자의 의도가 담긴 요청을 하게 만드는 공격
- 로그인된 사용자의 요청에 공격자가 원하는 요청으로 유도하는 공격 방법입니다.
공격 가능 예시를 살펴보겠습니다.
잘못쓴 GET 요청
게시판 서비스에서 모 개발자가 게시글을 삭제하는 URI를 다음과 같이 정의했습니다.
GET https://gucoding.com/post/delete/1
모 개발자는 해당 요청을 작성자 본인만 보낼 수 있게 설계해서 안전하다고 생각했습니다. 그러나 이것은 틀렸습니다.
공격자는 작성자 본인은 의도하지 않게 요청을 보내도록 의도할 수 있습니다.
예를들어, 공격자가 공격을 위한 게시글을 하나 올립니다.
해당 게시글에 이미지를 첨부하는데, 이 때 이미지 태그에
<img src="https://gucoding.com/post/delete/1" />
다음 html 태그를 첨부한다고 했을 때, 해당 post를 올린 작성자가 게시글을 조회하는 행동만으로 게시글을 삭제할 수 있게됩니다.
모 개발자는 GET 요청에 행위를 포함하지 않아야겠다고 깨달았습니다.
Form 요청 태그
모 개발자는 정신을 차리고 황급하게 게시글 삭제를 form 요청으로 바꿨습니다. 그러나 얼마못가 공격자에게 당하고 맙니다.
<html>
<head>
<title>흥미로운 뉴스 기사</title>
</head>
<body>
<h1>새로운 뉴스 기사를 확인하세요!</h1>
<img src="https://example.com/images/news.jpg" alt="뉴스">
<form id="csrfDeleteForm" action="https://gucoding.com/post/1" method="DELETE">
<input type="hidden" name="postId" value="123">
</form>
<script>
// 페이지 로드 시 자동으로 폼 제출 (예: 뉴스 기사 로딩 중 백그라운드에서 실행)
window.onload = function() {
document.getElementById('csrfDeleteForm').submit();
};
</script>
</body>
</html>
위와 마찬가지로 해당 게시글의 주인이 해당 기사를 조회하는 것 만으로도 본인의 게시글을 지우도록 만들 수 있습니다.
이에 대한 해결방안으로 CSRF 토큰이 존재합니다. 스프링 시큐리티에서는 필터체인 등록 시, csrf() 설정이 default로 잡혀있습니다.
<form action="https://example.com/api/updateProfile" method="POST">
<input type="hidden" name="csrf_token" value="[유효한_CSRF_토큰]"> <button type="submit">내 정보 업데이트</button>
</form>
해당 설정은 로그인한 유저에게 고유의 난수 토큰을 발급해주고 hidden 파라미터로 담은 요청만을 정상적인 요청으로 처리합니다.
난 왜 CSRF 보안로직을 쓴적이 없을까?
지금까지 경험에 의하면 프론트분들은 액세스 토큰을 메모리 혹은 로컬 스토리지에 저장하곤 했습니다. 왜냐하면 JWT 방식으로 인증했기 때문입니다.
만약에 쿠키-세션 방식으로 인증한다면 대부분의 쿠키는 브라우저에 의해 매 요청마다 필수적으로 전송이 됩니다.
💡
SameSite 옵션에 따라 동일 사이트 요청에서만 전송된다거나,
Secure 옵션으로 HTTPS 연결에서만 전송된다거나,
Domain 옵션으로 특정 도메인만 유효한다던가 하는 등
공격자는 이미 인증되고 있는 환경에서 악성요청을 동작하게만 구현하면되니 다소 쉬운 환경입니다.
제가 포스팅을 하게 된 계기가 액세스토큰도 쿠키에 두면 더 안전한거 아닌가?라는 생각으로 시작하게 됐는데요, 액세스토큰을 쿠키에 두지 않는 이유 중 하나가 아닐까하네요.
💡
물론 CSRF 공격을 SameSite 옵션을 줘서 방지한다면 다시금 쿠키에 저장하는게 안전하지 않을까? 라는 생각이 들었습니다.
- strict : 현재 브라우저 URL과 쿠키 도메인과 일치해야 전송가능
- Lax : 현재 브라우저 URL과 달라도 일부케이스(e.g.링크)에서는 접근 허용
그러나 제 경험상 프론트와 백엔드가 각자 배포해서 소통함으로 samesite=NONE 옵션을 줄 수 밖에 없었습니다.
얕게 알아본결과 리버스 프록시를 통해서 동일 도메인 환경을 구축가능한걸로 보입니다.
server {
listen 80;
server_name example.com;
# 프론트엔드 정적 파일 서빙 (Nginx 서버 자체에 프론트엔드 빌드 파일이 있을 경우)
location / {
root /var/www/frontend_app; # Nginx 서버의 로컬 경로
try_files $uri $uri/ /index.html;
}
# API 요청을 원격 백엔드 서버로 프록시
location /api/ {
# 백엔드 서버의 내부 IP 주소 또는 도메인 이름과 포트
proxy_pass http://192.168.1.100:8080/;
# 또는 도메인 이름: http://api.example.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
쿠키에 보안옵션을 빡세게 줄 수 있어도,
작은 프로젝트에서 개발시간을 지체하면서까지 그러고 싶지는 않은것도 하나의 이유일 것 같습니다.
XSS (Cross Site Scripting)
클라이언트 측에 스크립트를 삽입해서 정상적인 유저가 해당 로직을 실행하도록 하는 공격
@Controller
public class MyController {
@GetMapping("/search/{query}")
public String search(@PathVariable String query, Model model) {
model.addAttribute("query", query);
return "search";
}
}
{query} 에 악성 스크립트를 담아 보낼 때, 개발자가 단순표기가 아니라 스크립트가 실행될 수 있게 만든다면 문제가 됩니다.
물론 저는 Spring MVC를 사용할 것 같지는 않아 깊게 살펴보지는 않고 관련 링크를 남기면서 끝내겠습니다.