REST API 중심 규칙에 따라 개발 환경을 구축하려다 보니 GET/POST 외에 form에서 막히는 부분에 대하여 좀 더 구체적으로 이해해보고 싶어졌다.
왜 막아놓았을까? 그 필요성에 대해 알기 위해서는 우선 취약점을 통한 악용 사례에 먼저 접근해본다면 이해가 쉬울 거 같았다.
사이트 간 요청 위조(csrf) 취약점을 악용할 수 있는 방법을 살펴보자.
POST 요청을 통해 애플리케이션에 등록된 사용자가 자신의 이메일 주소를 변경할 수 있는 /user/email 라는 경로가 있다고 생각해 보자. 사용자는 email이라는 입력 요소에 사용하고자 하는 이메일 주소를 넣고 제출할 것이다.
이떄 만약 CSRF 보호가 없다면 악의적인 웹사이트에서 이 애플리케이션의 /user/email 경로로 요청을 보낼 수 있는 HTML 폼을 임의로 만들어 사용자의 이메일 주소를 악성 사용자의 이메일 주소로 변경할 수 있게 된다.
<form action="https://your-application.com/user/email" method="POST">
<input type="email" value="malicious-email@example.com">
</form>
<script>
document.forms[0].submit();
</script>
악성 사용자가 자동으로 폼을 제출하는 악의적인 웹사이트를 만든 뒤 일반 사용자를 유인하여 성공한다면 해당 사용자의 이메일 주소를 악성 사용자의 이메일 주소로 변경 할 수 있는 것이다.
정리하자면 CSRF 공격(Cross Site Request Forgery)은 웹 어플리케이션 취약점 중 하나로 인터넷 사용자(희생자)가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 만드는 공격이다.
따라서 이 취약점을 방지하기 위해서는 POST, PUT, PATCH, DELETE와 같은 요청을 보낼 때 악성 애플리케이션이 접근할 수 없는 비밀 세션 값이 유효한지 검사가 필요하다.
그리하여 인증된 사용자를 대신해서 승인되지 않은 커맨드를 악의적으로 활용하는 사이트 간 요청 위조(CSRF)에 대해서 라라벨은 크로스-사이트 요청 위조 공격 (CSRF)으로부터 애플리케이션을 손쉽게 보호할 수 있도록 해준다고 한다.
Laravel은 PHP 언어로 작성된 PHP로 웹개발을 하기위한 풀스텍 웹 프레임워크이며 오픈소스이다. 2011년 6월 첫 버전이 출시되었다.
2020년 기준 스택오버플로우 개발자 설문조사에서 PHP 프레임워크 중에서 인기가 높다. 심포니 프레임워크의 컴포넌트와 그 밖에 컴포넌트들로 모듈화 되어있다.
라라벨은 애플리케이션에서 관리하는 사용자의 세션마다 CSRF 토큰을 자동으로 생성한다. 이 토큰은 인증된 사용자가 실제로 애플리케이션에서 요청을 했는지 확인하는 데 사용되며 사용자의 세션에 저장되어 세션이 재생성 될 때마다 변경되므로 악성 애플리케이션이 접근을 막을 수 있게 된다.
애플리케이션에서 POST, PUT, PATCH, DELETE 폼을 만들 때 _token명칭을 가진 hidden타입의 입력 필드를 포함시켜 CSRF 보호 미들웨어가 요청을 확인할 수 있도록 한다.
<form method="POST" action="/user/email">
<input type="hidden" name="_token" value="{{ csrf_token() }}" >
</form>
기본적으로 web 미들웨어 그룹에 포함된 App\Http\Middleware\VerifyCsrfToken 미들웨어는 요청에 포함된 토큰이 일치하는지 자동으로 확인한다. 세션에 저장된 값과 토큰이 일치하면 인증된 사용자가 요청을 보낸 사용자라는 것을 알 수 있다.
HTTP Method POST를 보낼 경우 종종 302 error를 마주해본 적이 있을 것이다.
스프링 시큐리티의 CSRF(Cross-Site Request Forgery) 설정 때문이라고 한다.
스프링 시큐리티에서는 csrf 설정이 기본적으로 enabled 되어 있기 때문에 csrf 에 대한 토큰을 받도록 명시되어 있으며 GET 요청이 아닌 요청, 그러니깐 POST/PUT/DELETE/PATCH 는 CSRF 토큰이 포함 되어 있어야만 서버에 요청이 가능한 것이다.
필요한 요청에 CSRF 토큰을 담거나, 혹은 Spring Security의 기본 csrf 설정을 disable 시키는 방법이 있다.
일반적인 유저로부터 일반적인 브라우저 요청을 받을 때에는 CSRF 방어가 추천된다고 한다. 다만, non-browser client 를 통한 서비스를 만들 경우에는 disable 시키는게 필요 할 거라고 한다.
반면 Spring의 View Template 역할을 하는 타임리프는 개발자가 별도로 생성하지 않아도 POST 요청을 할 때 CSRF Token을 자동으로 생성해준다고 한다. 또한 스프링의 FORM 태그를 사용할 때도 CSRF TOKEN을 자동으로 생성해준다고 한다. 그렇지만 JSP 같은 경우에는 지원을 해주지 않기 때문에 사용하기 위해서는 Form Tag에 hidden 값으로 _csrf를 넣어줘야한다고 한다.
Spring Security가 CSRF 공격으로부터 애플리케이션을 보호하는 방법에 대해 논의하기 전에 CSRF 공격이 무엇인지 설명하겠습니다. 더 잘 이해하기 위해 구체적인 예를 살펴봅시다.
은행 웹 사이트에서 현재 로그인한 사용자의 돈을 다른 은행 계좌로 이체할 수 있는 양식을 제공한다고 가정합니다. 예를 들어 HTTP 요청은 다음과 같습니다.
이제 은행 웹 사이트에 인증한 것처럼 가장한 다음 로그아웃하지 않고 악의적인 웹 사이트를 방문하십시오. 악의적인 웹 사이트에는 다음과 같은 형식의 HTML 페이지가 포함되어 있습니다.
<form action="https://bank.example.com/transfer" method="post">
<input type="hidden"
name="amount"
value="100.00"/>
<input type="hidden"
name="routingNumber"
value="evilsRoutingNumber"/>
<input type="hidden"
name="account"
value="evilsAccountNumber"/>
<input type="submit"
value="Win Money!"/>
</form>
당신은 돈을 따는 것을 좋아하기 때문에 제출 버튼을 클릭한다. 이 과정에서 당신은 악의 있는 사용자에게 의도치 않게 100달러를 송금했다. 이 문제는 악의적인 웹 사이트가 사용자의 쿠키를 볼 수 없지만 은행과 연결된 쿠키가 요청과 함께 계속 전송되기 때문에 발생합니다.
최악의 경우, 이 모든 과정이 자바스크립트를 사용하여 자동화될 수 있었다. 이것은 당신이 그 버튼을 클릭할 필요가 없었다는 것을 의미한다. 그렇다면 우리는 어떻게 그러한 공격으로부터 우리 자신을 보호할 수 있을까요?
문제는 은행 홈페이지의 HTTP 요청과 악의 홈페이지의 요청이 정확히 일치한다는 점이다. 이는 사악한 웹사이트에서 오는 요청을 거절하고 은행의 웹사이트에서 오는 요청을 허용할 방법이 없다는 것을 의미한다. CSRF 공격으로부터 보호하기 위해 우리는 요청에 악의적인 사이트가 제공할 수 없는 무언가가 있는지 확인해야 합니다.
한 가지 해결책은 싱크로나이저 토큰 패턴을 사용하는 것이다. 이 솔루션은 각 요청에 세션 쿠키 외에도 임의로 생성된 토큰이 HTTP 매개 변수로 요구되도록 하는 것입니다. 요청이 제출되면 서버는 매개변수에 대한 기대 값을 조회하고 요청의 실제 값과 비교해야 합니다. 값이 일치하지 않으면 요청이 실패합니다.
상태를 업데이트하는 각 HTTP 요청에 대한 토큰만 요구하도록 기대를 완화할 수 있습니다. 동일한 오리진 정책이 악의적인 사이트가 응답을 읽을 수 없도록 보장하므로 안전하게 이 작업을 수행할 수 있습니다. 또한 랜덤 토큰은 HTTP GET에 포함시키지 않습니다. 이는 토큰이 유출될 수 있기 때문입니다.
우리의 예가 어떻게 바뀔지 살펴보자. 무작위로 생성된 토큰이 _csrf라는 HTTP 매개 변수에 있다고 가정합니다. 예를 들어, 송금 요청은 다음과 같습니다.
_csrf 매개 변수와 랜덤 값을 추가했습니다. 이제 악의 웹 사이트는 _csrf 매개 변수(악의 웹 사이트에 명시적으로 제공되어야 함)에 대한 올바른 값을 추측할 수 없으며 서버가 실제 토큰을 예상 토큰과 비교할 때 전송이 실패합니다.
언제 CSRF 보호를 사용해야 합니까? 일반 사용자가 브라우저에서 처리할 수 있는 요청에 대해 CSRF 보호를 사용하는 것이 좋습니다. 브라우저가 아닌 클라이언트에서 사용하는 서비스만 만드는 경우 CSRF 보호를 사용하지 않도록 설정할 수 있습니다.
일반적인 질문은 "javascript에 의해 만들어진 JSON 요청을 보호해야 합니까?"이다. 간단한 대답은, 그건 상황에 따라 다릅니다. 그러나 JSON 요청에 영향을 미칠 수 있는 CSRF 익스플로잇이 있으므로 매우 주의해야 합니다. 예를 들어 악의적인 사용자는 다음 양식을 사용하여 JSON으로 CSRF를 생성할 수 있습니다.
<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
value="Win Money!"/>
</form>
이는 곧 다음과 같은 JSON 구조를 생성할 것입니다.
{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}
응용 프로그램이 콘텐츠 유형을 검증하지 않는 경우 이 취약성에 노출될 수 있습니다. 또한 설정에 따라 Content-Type을 검증하는 Spring MVC 응용 프로그램은 URL 접미사가 ".json"으로 끝나도록 업데이트하여 악용될 수도 있습니다.
응용 프로그램이 상태 비저장 상태이면 어떻게 합니까? 그것은 반드시 당신이 보호받는다는 것을 의미하지는 않는다. 실제로 사용자가 특정 요청에 대해 웹 브라우저에서 어떤 작업도 수행할 필요가 없다면 CSRF 공격에 여전히 취약할 수 있습니다.
예를 들어, 응용프로그램이 인증에 JSESSIONID 대신 모든 상태를 포함하는 사용자 정의 쿠키를 사용한다고 가정합니다. CSRF 공격이 발생하면 이전 예에서 JSESSIONID 쿠키가 전송된 것과 동일한 방식으로 사용자 지정 쿠키가 요청과 함께 전송됩니다.
기본 인증을 사용하는 사용자도 CSRF 공격에 취약합니다. 브라우저는 이전 예에서 JSESSIONID 쿠키가 전송된 것과 같은 방식으로 사용자 이름 암호를 모든 요청에 자동으로 포함하기 때문입니다.
Spring Security의 CSRF 보호를 사용하는 단계는 다음과 같습니다.
CSRF 공격으로부터 보호하는 첫 번째 단계는 웹 사이트가 적절한 HTTP 동사를 사용하는지 확인하는 것입니다. 특히 Spring Security의 CSRF 지원을 사용하려면 응용 프로그램에서 상태를 수정하는 모든 항목에 PATCH, POST, PUT 및/또는 DELETE를 사용하고 있는지 확인해야 합니다.
이는 Spring Security의 지원의 제한이 아니라 적절한 CSRF 예방을 위한 일반적인 요구 사항입니다. 그 이유는 HTTP GET에 개인 정보를 포함하면 정보가 유출될 수 있기 때문입니다. 중요한 정보는 GET 대신 POST를 사용하는 일반적인 지침.
다음 단계는 Spring Security의 CSRF 보호를 응용 프로그램에 포함하는 것입니다. 일부 프레임워크는 사용자 세션을 무효화하여 유효하지 않은 CSRF 토큰을 처리하지만, 이로 인해 자체적인 문제가 발생합니다. 대신 기본적으로 Spring Security의 CSRF 보호는 HTTP 403 액세스를 거부합니다. InvalidCsrfTokenException을 다르게 처리하도록 AccessDeniedHandler를 구성하여 이 설정을 사용자 지정할 수 있습니다.
Spring Security 4.0 이후 CSRF 보호는 XML 구성에서 기본적으로 활성화됩니다. CSRF 보호를 비활성화하려면 아래 해당 XML 구성을 볼 수 있습니다.
CSRF 보호는 Java Configuration에서 기본적으로 활성화됩니다. CSRF를 비활성화하려면 아래 해당 Java 구성을 볼 수 있습니다. CSRF 보호 구성 방법에 대한 추가 사용자 지정은 csrf()의 Javadoc을 참조하십시오.
마지막 단계는 모든 Patch, POST, PUT 및 DELETE 메서드에 CSRF 토큰을 포함하는 것입니다. 이에 접근하는 한 가지 방법은 _csrf 요청 속성을 사용하여 현재 CsrfToken을 얻는 것입니다. JSP를 사용하여 이 작업을 수행하는 예는 다음과 같습니다.
<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
method="post">
<input type="submit"
value="Log out" />
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
</form>
더 쉬운 방법은 Spring Security JSP 태그 라이브러리의 csrfInput 태그를 사용하는 것입니다.
Ajax 및 JSON 요청
JSON을 사용하는 경우 HTTP 매개 변수 내에서 CSRF 토큰을 제출할 수 없습니다. 대신 HTTP 헤더 내에서 토큰을 제출할 수 있습니다. 일반적인 패턴은 메타 태그에 CSRF 토큰을 포함하는 것입니다. JSP의 예는 다음과 같습니다.
<html>
<head>
<meta name="_csrf" content="${_csrf.token}"/>
<!-- default header name is X-CSRF-TOKEN -->
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<!-- ... -->
</head>
<!-- ... -->
jQuery를 사용하는 경우 다음 작업을 수행할 수 있습니다.
$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
});
#### ```
- 제한 시간
한 가지 문제는 예상되는 CSRF 토큰이 HttpSession에 저장되므로 HttpSession이 만료되는 즉시 구성된 AccessDeniedHandler가 InvalidCsrfTokenException을 수신한다는 것입니다. 기본 AccessDeniedHandler를 사용하는 경우 브라우저는 HTTP 403을 수신하고 오류 메시지를 표시합니다.
Multipart (file upload) 관련 내용도 있으니 추후 참고
spring-security - 참고 자료
안녕하세요, csrf 공부 중 설명이 너무 좋은 글을 보게 되었습니다.
그 중 질문드리고 싶은 내용이 있는데,
혹시 위의 첫번째 예문에서 document.forms[0].submit();스크립트 구문을 작성한 이유에 대해서 알려주실 수 있으실까요?