[security] CSRF 예방 토큰을 쿠키에 넣는 것이 왜 흔한가요?

CSRF의 모든 문제와 그것을 방지하는 적절한 방법을 이해하려고합니다. (내가 읽고 이해하고 동의 한 리소스 : OWASP CSRF Prevention CHeat Sheet , CSRF에 대한 질문 )

내가 이해하는 것처럼 CSRF와 관련된 취약점은 웹 서버의 관점에서 들어오는 HTTP 요청의 유효한 세션 쿠키가 인증 된 사용자의 희망을 반영한다고 가정하여 도입되었습니다. 그러나 원본 도메인에 대한 모든 쿠키는 브라우저에 의해 요청에 마술처럼 첨부되므로 실제로 모든 서버는 요청에 유효한 세션 쿠키가 있음을 유추 할 수 있습니다. 요청이 인증 된 세션을 가진 브라우저에서 온 것입니다. 코드 에 대해 더 이상 가정 할 수 없습니다해당 브라우저에서 실행하거나 사용자가 원하는 것을 실제로 반영하는지 여부 이를 방지하는 방법은 요청에 브라우저의 자동 쿠키 처리 이외의 다른 방법으로 추가 인증 정보 ( “CSRF 토큰”)를 포함시키는 것입니다. 느슨하게 말하면 세션 쿠키는 사용자 / 브라우저를 인증하고 CSRF 토큰은 브라우저에서 실행되는 코드를 인증합니다.

간단히 말해 세션 쿠키를 사용하여 웹 응용 프로그램의 사용자를 인증하는 경우 각 응답에 CSRF 토큰을 추가하고 각 (변경) 요청마다 일치하는 CSRF 토큰이 필요합니다. 그런 다음 CSRF 토큰은 서버에서 브라우저로 다시 왕복하여 서버로 요청을하는 페이지가 해당 서버에 의해 승인됨을 증명합니다.

내 질문에 대해서는 왕복에서 해당 CSRF 토큰에 사용되는 특정 전송 방법에 관한 것입니다.

AngularJS , Django , Rails 에서 CSRF 토큰을 쿠키 (예 : Set-Cookie 헤더)로 서버에서 클라이언트로 보낸 다음 클라이언트의 Javascript를 쿠키에서 긁어내어 첨부하는 것이 일반적입니다 (예 : AngularJS , Django , Rails ). 서버로 다시 보낼 별도의 XSRF-TOKEN 헤더로.

(대체 방법은 예를 들어 Express 에서 권장하는 방법입니다. 여기서 서버에서 생성 한 CSRF 토큰은 서버 측 템플릿 확장을 통해 응답 본문에 포함되어 서버로 다시 공급되는 코드 / 마크 업에 직접 연결됩니다. 이 예는 웹을 사용하는 1.0-ish 방식이지만 JS가 많은 클라이언트에게는 일반적으로 적합합니다.)

CSRF 토큰의 다운 스트림 전송으로 Set-Cookie를 사용하는 것이 왜 일반적입니까? 이것이 좋은 생각입니까? 나는이 모든 프레임 워크의 저자들이 그들의 선택을 신중하게 고려했지만 이것이 잘못되지 않았다고 생각합니다. 그러나 언뜻보기에는 쿠키를 사용하여 쿠키의 디자인 제한 사항을 해결하는 것이 어려워 보입니다. 실제로 쿠키를 왕복 전송으로 사용하면 (서버가 브라우저에 CSRF 토큰을 알리기 위해 Set-Cookie : 헤더 다운 스트림, 브라우저가 서버로 반환하기위한 Cookie : 헤더 업스트림) 쿠키를 사용하면 취약점이 다시 나타납니다. 해결하려고합니다.

위의 프레임 워크는 CSRF 토큰의 전체 왕복에 쿠키를 사용하지 않습니다. 이들은 Set-Cookie 다운 스트림을 사용하고 그 다음에 다른 것 (예 : X-CSRF-Token 헤더)을 업스트림으로 사용하므로이 취약점을 차단합니다. 그러나 다운 스트림 전송으로 Set-Cookie를 사용하더라도 오해의 소지가 있으며 위험합니다. 브라우저는 이제 진짜 악성 XSRF 요청을 포함한 모든 요청에 ​​CSRF 토큰을 첨부합니다. 기껏해야 요청을 요구보다 커지게하고 최악의 경우 서버 코드를 잘못 사용하여 실제로 사용하려고 시도 할 수 있습니다. 또한 CSRF 토큰의 실제 수신자는 클라이언트 측 Javascript이므로이 쿠키는 http 전용으로 보호 할 수 없습니다.



답변

CSRF 쿠키를받은 후에는 클라이언트 스크립트의 응용 프로그램 전체에서 일반 형식과 AJAX POST 모두에서 사용할 수 있도록 쿠키를 사용할 수 있기 때문입니다. 이것은 AngularJS에서 사용하는 것과 같이 JavaScript가 많은 응용 프로그램에서 의미가 있습니다 (AngularJS를 사용하면 응용 프로그램이 단일 페이지 응용 프로그램 일 필요는 없으므로 CSRF 값이있는 다른 페이지 요청간에 상태가 이동 해야하는 경우 유용합니다) 브라우저에서 정상적으로 지속될 수 없습니다).

설명하는 각 방법의 장단점에 대해 일반적인 응용 프로그램에서 다음 시나리오와 프로세스를 고려하십시오. 이것들은 Synchronizer 토큰 패턴을 기반으로합니다 .

요청 본문 접근

  1. 사용자가 성공적으로 로그인했습니다.
  2. 서버가 인증 쿠키를 발행합니다.
  3. 사용자가 클릭하여 양식으로 이동합니다.
  4. 이 세션에 대해 아직 생성되지 않은 경우 서버는 CSRF 토큰을 생성하고이를 사용자 세션에 대해 저장 한 후 숨겨진 필드로 출력합니다.
  5. 사용자가 양식을 제출합니다.
  6. 서버가 숨겨진 필드가 세션 저장 토큰과 일치하는지 확인합니다.

장점 :

  • 구현이 간단합니다.
  • AJAX와 함께 작동합니다.
  • 양식과 함께 작동합니다.
  • 쿠키는 실제로 HTTP 전용 일 수 있습니다 .

단점 :

  • 모든 양식은 HTML로 숨겨진 필드를 출력해야합니다.
  • 모든 AJAX POST에도 값이 포함되어야합니다.
  • 페이지는 CSRF 토큰이 필요하다는 것을 미리 알고 있어야만 페이지 컨텐츠에 포함 할 수 있으므로 모든 페이지에는 어딘가에 토큰 값이 포함되어야하므로 대규모 사이트에 구현하는 데 시간이 오래 걸릴 수 있습니다.

맞춤 HTTP 헤더 (다운 스트림)

  1. 사용자가 성공적으로 로그인했습니다.
  2. 서버가 인증 쿠키를 발행합니다.
  3. 사용자가 클릭하여 양식으로 이동합니다.
  4. 브라우저에 페이지가로드 된 후 CSRF 토큰을 검색하기 위해 AJAX 요청이 작성됩니다.
  5. 서버는 CSRF 토큰을 생성하고 (세션에 대해 아직 생성되지 않은 경우) 사용자 세션에 대해 저장하고 헤더로 출력합니다.
  6. 사용자가 양식을 제출합니다 (토큰은 숨겨진 필드를 통해 전송 됨).
  7. 서버가 숨겨진 필드가 세션 저장 토큰과 일치하는지 확인합니다.

장점 :

  • AJAX와 함께 작동합니다.
  • 쿠키는 HTTP 전용 일 수 있습니다 .

단점 :

  • 헤더 값을 얻기 위해 AJAX 요청이 없으면 작동하지 않습니다.
  • 모든 양식에는 HTML에 동적으로 추가 된 값이 있어야합니다.
  • 모든 AJAX POST에도 값이 포함되어야합니다.
  • CSRF 토큰을 얻기 위해 페이지에서 AJAX 요청을 먼저해야하므로 매번 추가 왕복이 발생합니다.
  • 추가 요청을 저장하는 페이지에 단순히 토큰을 출력했을 수도 있습니다.

맞춤 HTTP 헤더 (업스트림)

  1. 사용자가 성공적으로 로그인했습니다.
  2. 서버가 인증 쿠키를 발행합니다.
  3. 사용자가 클릭하여 양식으로 이동합니다.
  4. 이 세션에 대해 아직 생성되지 않은 경우 서버는 CSRF 토큰을 생성하고이를 사용자 세션에 대해 저장 한 후 페이지 컨텐츠에 출력합니다.
  5. 사용자는 AJAX를 통해 양식을 제출합니다 (토큰은 헤더를 통해 전송 됨).
  6. 서버가 사용자 정의 헤더가 세션 저장 토큰과 일치하는지 확인합니다.

장점 :

  • AJAX와 함께 작동합니다.
  • 쿠키는 HTTP 전용 일 수 있습니다 .

단점 :

  • 양식에서는 작동하지 않습니다.
  • 모든 AJAX POST에는 헤더가 포함되어야합니다.

사용자 정의 HTTP 헤더 (업스트림 및 다운 스트림)

  1. 사용자가 성공적으로 로그인했습니다.
  2. 서버가 인증 쿠키를 발행합니다.
  3. 사용자가 클릭하여 양식으로 이동합니다.
  4. 브라우저에 페이지가로드 된 후 CSRF 토큰을 검색하기 위해 AJAX 요청이 작성됩니다.
  5. 서버는 CSRF 토큰을 생성하고 (세션에 대해 아직 생성되지 않은 경우) 사용자 세션에 대해 저장하고 헤더로 출력합니다.
  6. 사용자는 AJAX를 통해 양식을 제출합니다 (토큰은 헤더를 통해 전송 됨).
  7. 서버가 사용자 정의 헤더가 세션 저장 토큰과 일치하는지 확인합니다.

장점 :

  • AJAX와 함께 작동합니다.
  • 쿠키는 HTTP 전용 일 수 있습니다 .

단점 :

  • 양식에서는 작동하지 않습니다.
  • 모든 AJAX POST에도 값이 포함되어야합니다.
  • CRSF 토큰을 얻기 위해 페이지에서 AJAX 요청을 먼저해야하므로 매번 추가 왕복이 발생합니다.

세트 쿠키

  1. 사용자가 성공적으로 로그인했습니다.
  2. 서버가 인증 쿠키를 발행합니다.
  3. 사용자가 클릭하여 양식으로 이동합니다.
  4. 서버는 CSRF 토큰을 생성하여 사용자 세션에 저장하고 쿠키로 출력합니다.
  5. 사용자는 AJAX 또는 HTML 양식을 통해 양식을 제출합니다.
  6. 서버는 사용자 정의 헤더 (또는 숨겨진 양식 필드)가 세션 저장 토큰과 일치하는지 확인합니다.
  7. CSRF 토큰을 검색하기 위해 서버에 추가 요청없이 추가 AJAX 및 양식 요청에 사용할 수 있도록 브라우저에서 쿠키를 사용할 수 있습니다.

장점 :

  • 구현이 간단합니다.
  • AJAX와 함께 작동합니다.
  • 양식과 함께 작동합니다.
  • 쿠키 값을 얻기 위해 반드시 AJAX 요청이 필요하지는 않습니다. 모든 HTTP 요청은이를 검색 할 수 있으며 JavaScript를 통해 모든 양식 / AJAX 요청에 추가 될 수 있습니다.
  • CSRF 토큰이 검색되면 쿠키에 저장되므로 추가 요청없이 값을 재사용 할 수 있습니다.

단점 :

  • 모든 양식에는 HTML에 동적으로 추가 된 값이 있어야합니다.
  • 모든 AJAX POST에도 값이 포함되어야합니다.
  • 쿠키는 요청 크기가 증가 할 때마다 요청 (즉, 이미지, CSS, JS 등에 대한 모든 GET, CSRF 프로세스에 포함되지 않음)에 대해 제출됩니다.
  • 쿠키는 HTTP 전용 일 수 없습니다 .

따라서 쿠키 접근 방식은 매우 동적으로 쿠키 값 (모든 HTTP 요청)을 검색하고이를 사용할 수있는 쉬운 방법을 제공합니다 (JS는 값을 자동으로 모든 양식에 추가 할 수 있으며 AJAX 요청에 헤더 또는 양식 값). CSRF 토큰이 세션에 수신되면 CSRF 익스플로잇을 사용하는 공격자가이 토큰을 검색 할 방법이 없으므로이를 재생성 할 필요가 없습니다. 악의적 인 사용자가 위의 방법 중 하나로 사용자의 CSRF 토큰을 읽으려고하면 Same Origin Policy에 의해 방지됩니다 . 악의적 인 사용자가 CSRF 토큰 서버 측을 검색하려는 경우 (예 :curl)이 토큰은 피해자의 인증 세션 쿠키가 요청에서 누락 될 때와 동일한 사용자 계정에 연결되지 않습니다 (공격자가 될 것이므로 서버 측이 희생자의 세션과 연결되지 않음).

뿐만 아니라 동기화 토큰 패턴 또한이 두 번 제출 쿠키CSRF 방지 방법. 쿠키를 사용하여 CSRF 토큰 유형을 저장합니다. CSRF 토큰에 서버 측 상태가 필요하지 않으므로 구현하기가 더 쉽습니다. 실제로이 방법을 사용할 때 CSRF 토큰은 표준 인증 쿠키가 될 수 있으며이 값은 요청과 마찬가지로 쿠키를 통해 제출되지만 숨겨진 필드 나 헤더에서도 반복됩니다. 그들은 처음부터 가치를 읽을 수 없습니다. 그러나 HttpOnly로 표시되어 인증 쿠키를 보호 할 수 있도록 인증 쿠키 이외의 다른 쿠키를 선택하는 것이 좋습니다. 이것이 쿠키 기반 방법을 사용하여 CSRF 예방을 찾는 또 다른 일반적인 이유입니다.


답변

쿠키를 사용하여 CSRF 토큰을 클라이언트에 제공하면 공격자가 쿠키 값을 읽을 수 없으므로 서버 측 CSRF 유효성 검사에서 요구하는 곳에 쿠키를 놓을 수 없으므로 공격이 성공하지 못합니다.

공격자는 요청 헤더에 인증 토큰 쿠키와 CSRF 쿠키를 모두 사용하여 서버에 요청을 할 수 있습니다. 그러나 서버는 요청 헤더에서 쿠키로 CSRF 토큰을 찾지 않고 요청의 페이로드를 찾습니다. 그리고 공격자가 CSRF 토큰을 페이로드에 넣을 위치를 알고 있더라도 그 값을 읽어야합니다. 그러나 브라우저의 교차 출처 정책은 대상 웹 사이트에서 쿠키 값을 읽는 것을 방지합니다.

서버가 요청 헤더에 쿠키를 예상하고 공격자가 쿠키 토큰을 넣을 특별한 작업을 수행 할 필요가 없기 때문에 동일한 논리가 인증 토큰 쿠키에 적용되지 않습니다.


답변

대답에 대한 최선의 추측 : CSRF 토큰을 서버에서 브라우저로 가져 오는 방법에 대한이 세 가지 옵션을 고려하십시오.

  1. 요청 본문에서 (HTTP 헤더가 아님)
  2. Set-Cookie가 아닌 사용자 지정 HTTP 헤더에서.
  3. 쿠키로서 Set-Cookie 헤더에 있습니다.

필자는 요청 본문 ( 질문에 링크 된 Express 자습서에서 보여준) 중 첫 번째 요청 이 다양한 상황에 적합하지 않다고 생각합니다. 모든 사람이 모든 HTTP 응답을 동적으로 생성하는 것은 아닙니다. 생성 된 응답에 토큰을 넣을 필요가있는 곳은 숨겨진 양식 입력, JS 코드 조각 또는 다른 JS 코드가 액세스 할 수있는 변수, 일반적으로 나쁜 곳 인 것처럼 보일 수도 있습니다. CSRF 토큰을 넣으려면). 따라서 일부 사용자 정의 작업은 가능하지만 # 1은 모든 규모의 접근 방식을 수행하기 어려운 곳입니다.

두 번째 사용자 정의 헤더는 매력적이지만 실제로 작동하지 않습니다 .JS가 호출 한 XHR의 헤더를 가져올 수는 있지만로드 한 페이지의 헤더를 가져올 수 없기 때문입니다 .

세 번째 쿠키는 Set-Cookie 헤더가 제공하는 쿠키로 모든 상황에서 사용하기 쉬운 접근 방식으로 남습니다. 데이터는 요청 본문에 있습니다). 따라서 단점에도 불구하고 프레임 워크가 널리 구현되는 가장 쉬운 방법이었습니다.


답변

세션 쿠키 (표준)는 추가 쿠키를 사용하고 싶지 않습니다.

많은 AJAX 요청으로 단일 페이지 웹 응용 프로그램 (SPA)을 만들 때 나에게 맞는 솔루션을 찾았습니다. 참고 : 서버 측 Java 및 클라이언트 측 JQuery를 사용하고 있지만 마술은 없으므로이 원칙을 모든 인기있는 프로그래밍 언어로 구현할 수 있다고 생각합니다.

추가 쿠키가없는 내 솔루션은 간단합니다.

고객 입장에서

성공적인 로그인 후 서버가 반환 한 CSRF 토큰을 전역 변수에 저장하십시오 (물론 전역 저장소 대신 웹 저장소를 사용하려는 경우). 각 AJAX 호출에서 X-CSRF-TOKEN 헤더를 제공하도록 JQuery에 지시하십시오.

기본 “색인”페이지에는 다음 JavaScript 스 니펫이 포함되어 있습니다.

// Intialize global variable CSRF_TOKEN to empty sting. 
// This variable is set after a succesful login
window.CSRF_TOKEN = '';

// the supplied callback to .ajaxSend() is called before an Ajax request is sent
$( document ).ajaxSend( function( event, jqXHR ) {
    jqXHR.setRequestHeader('X-CSRF-TOKEN', window.CSRF_TOKEN);
}); 

서버 측

로그인에 성공하면 임의의 (그리고 충분히 긴) CSRF 토큰을 생성하고이를 서버 측 세션에 저장 한 후 클라이언트에 반환하십시오. X-CSRF-TOKEN 헤더 값을 세션에 저장된 값과 비교하여 특정 (민감한) 수신 요청을 필터링하십시오. 일치해야합니다.

민감한 AJAX 호출 (POST 양식 데이터 및 GET JSON 데이터) 및이를 포착하는 서버 측 필터는 / dataservice / * 경로에 있습니다. 로그인 요청은 필터에 맞지 않아야하므로 다른 경로에 있습니다. HTML, CSS, JS 및 이미지 리소스에 대한 요청도 / dataservice / * 경로에 없으므로 필터링되지 않습니다. 이것들은 비밀이 없으며 해를 끼칠 수 없으므로 괜찮습니다.

@WebFilter(urlPatterns = {"/dataservice/*"})
...
String sessionCSRFToken = req.getSession().getAttribute("CSRFToken") != null ? (String) req.getSession().getAttribute("CSRFToken") : null;
if (sessionCSRFToken == null || req.getHeader("X-CSRF-TOKEN") == null || !req.getHeader("X-CSRF-TOKEN").equals(sessionCSRFToken)) {
    resp.sendError(401);
} else
    chain.doFilter(request, response);
}   


답변