보안: 크로스 사이트 요청 위조(Cross-Site Request Forgery) 공격 방지하기

등록일시: 2014-02-03 08:00,  수정일시: 2014-01-28 23:38
조회수: 45,386
이 문서는 ASP.NET Web API 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.

크로스 사이트 요청 위조(CSRF, Cross-Site Request Forgery) 공격은 사용자가 현재 로그인해 있는 취약한 사이트로 악의적인 사이트에서 요청을 전송하는 공격입니다.

가령, 다음은 CSRF 공격의 한 가지 사례입니다:

  1. 사용자가 폼 인증을 통해서 www.example.com에 로그인합니다.
  2. 서버가 사용자를 인증합니다. 이 때, 서버에서 반환된 응답에는 인증 쿠키가 포함되어 있습니다.
  3. 사용자가 로그아웃하지 않은 상태에서 악의적인 사이트를 방문합니다. 그리고, 이 악의적인 사이트에 다음과 같은 HTML 폼이 존재한다고 가정해보겠습니다:
    <h1>You Are a Winner!</h1>
    <form action="http://example.com/api/account" method="post">
        <input type="hidden" name="Transaction" value="withdraw" />
        <input type="hidden" name="Amount" value="1000000" />
        <input type="submit" value="Click Me"/>
    </form>

    이 HTML 폼의 action 어트리뷰트가 악의적인 사이트가 아닌, 취약한 사이트를 가리키고 있다는 점에 유의하시기 바랍니다. 바로 이런 특징 때문에, "크로스 사이트(Cross-Site)"라는 명칭이 붙은 것입니다.

  4. 사용자가 "submit" 버튼을 클릭합니다. 그러면, 브라우저가 요청에 인증 쿠키를 함께 포함시켜서 전송합니다.
  5. 이 요청은 서버에서 사용자의 인증 컨텍스트로 실행되므로, 인증된 사용자가 수행할 수 있는 모든 작업을 수행할 수 있습니다.

비록, 이 예제에서는 사용자가 직접 폼 버튼을 클릭해야만 공격이 수행되지만, 악의적인 페이지에서 AJAX 요청을 전송하는 스크립트를 자동으로 실행하도록 만드는 일도 매우 간단합니다. 더군다나, 악의적인 사이트에서 "https://"로 요청을 전송할 수도 있기 때문에, SSL을 사용하더라도 CSRF 공격을 방지할 수는 없습니다.

대부분의 경우 CSRF 공격은 쿠키를 이용해서 인증을 수행하는 사이트들을 대상으로 행해지는데, 그 이유는 브라우저가 자동으로 모든 관련된 쿠키들을 목적지 웹 사이트로 전송하기 때문입니다. 그러나, 그렇다고 해서 반드시 쿠키를 악용하는 형태로만 CSRF 공격이 이루어지는 것은 아닙니다. 가령, 기본 인증과 다이제스트(Digest) 인증도 역시 CSRF 공격에 취약합니다. 즉, 사용자가 기본 인증이나 다이제스트 인증으로 로그인 한 이후, 브라우저가 자동으로 세션 종료 시까지 자격 증명을 함께 전송하기 때문입니다.

위조 방지 토큰(Anti-Forgery Tokens)

ASP.NET MVC에서는 CSRF 공격을 방지하기 위해서 위조 방지 토큰을 사용하는데, 이를 요청 검증 토큰(Request Verification Tokens)이라고 부르기도 합니다.

  1. 클라이언트가 폼이 존재하는 HTML 페이지를 요청합니다.
  2. 그러면, 서버는 응답에 두 가지 토큰을 포함시켜서 반환합니다. 그 중, 한 가지 토큰은 쿠키를 통해서 전송됩니다. 그리고, 다른 토큰은 숨겨진 폼 필드에 담겨집니다. 이 토큰들은 악의적인 사용자들이 값을 추측할 수 없도록 무작위로 생성됩니다.
  3. 클라이언트가 폼을 제출할 때, 두 가지 토큰이 모두 서버로 재전송돼야 합니다. 클라이언트는 쿠키 토큰은 쿠키를 통해서 전송하고, 폼 토큰은 폼 데이터에 담아서 전송합니다. (브라우저 클라이언트가 사용자가 폼을 제출할 때 자동으로 이 작업을 처리해줍니다.)
  4. 만약, 두 토큰 중 하나라도 일치하지 않으면 서버가 해당 요청을 허용하지 않습니다.

다음은 숨겨진 폼 토큰을 포함하고 있는 HTML 폼의 한 예입니다:

<form action="/Home/Test" method="post">
    <input name="__RequestVerificationToken" type="hidden"   
           value="6fGBtLZmVBZ59oUad1Fr33BuPxANKY9q3Srr5y[...]" />
    <input type="submit" value="Submit" />
</form>

위조 방지 토큰으로 CSRF 공격을 방지할 수 있는 이유는, Same-Origin 정책에 따라 악의적인 페이지에서 사용자의 토큰을 읽을 수 없기 때문입니다. (Same-Orgin 정책은 두 개의 다른 사이트에서 호스트되고 있는 문서들이 서로 상대방의 콘텐트에 접근하는 것을 막습니다. 그래서 첫 번째 예제에서 살펴본 것처럼 악의적인 페이지에서 example.com으로 요청을 전송할 수는 있어도, 응답을 읽을 수는 없습니다.)

사용자가 로그인 한 이후에 브라우저가 내부적으로 자격 증명을 전송하는 모든 인증 프로토콜들은 위조 방지 토큰을 사용해야만 CSRF 공격을 방지할 수 있습니다. 그 대상에는, 폼 인증 같은 쿠키 기반 인증 프로토콜뿐만 아니라 기본 인증이나 다이제스트 인증도 포함됩니다.

또한, 모든 안전하지 않은 메서드들(POST, PUT, DELETE)에 위조 방지 토큰을 적용해야 합니다. 그리고, 안전한 메서드들도(GET, HEAD) 부작용을 일으키지 않는지 확인해야 합니다. 더군다나, CORS나 JSONP 같은 크로스-도메인 지원을 활성화시켰다면, GET 같은 안전한 메서드조차도 잠재적으로 CSRF 공격에 취약점을 갖게 되므로 공격자가 내부적으로 민감한 데이터를 읽을 수 있게 됩니다.

ASP.NET MVC의 위조 방지 토큰

Razor 페이지에 위조 방지 토큰을 추가하려면 HtmlHelper.AntiForgeryToken 도우미 메서드를 사용합니다:

@using (Html.BeginForm("Manage", "Account")) {
    @Html.AntiForgeryToken()
}

이 메서드는 숨겨진 폼 필드를 추가해줄 뿐만 아니라 쿠키 토큰도 설정해줍니다.

CSRF 방지와 AJAX

보통 AJAX 요청은 HTML 폼 데이터 대신 JSON 데이터로 전송되는 경우가 많기 때문에, AJAX 요청에서는 폼 토큰이 문제가 될 수 있습니다. 이 문제를 해결할 수 있는 한 가지 방법은 토큰을 사용자 지정 HTTP 헤더에 담아서 전송하는 것입니다. 다음 코드는 Razor 구문으로 토큰들을 생성한 다음, 이를 AJAX 요청에 추가합니다. 토큰들은 AntiForgery.GetTokens 호출을 통해서 서버 측에서 생성됩니다.

<script>
    @functions{
        public string TokenHeaderValue()
        {
            string cookieToken, formToken;
            AntiForgery.GetTokens(null, out cookieToken, out formToken);
            return cookieToken + ":" + formToken;                
        }
    }

    $.ajax("api/values", {
        type: "post",
        contentType: "application/json",
        data: {  }, // JSON data goes here
        dataType: "json",
        headers: {
            'RequestVerificationToken': '@TokenHeaderValue()'
        }
    });
</script>

그리고 요청을 처리할 때, 요청 헤더에서 토큰들을 추출합니다. 그런 다음, AntiForgery.Validate 메서드를 호출해서 토큰들의 유효성을 검사합니다. 만약, 토큰이 유효하지 않으면 Validate 메서드에서 예외가 던져집니다.

void ValidateRequestHeader(HttpRequestMessage request)
{
    string cookieToken = "";
    string formToken = "";

    IEnumerable<string> tokenHeaders;
    if (request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
    {
        string[] tokens = tokenHeaders.First().Split(':');
        if (tokens.Length == 2)
        {
            cookieToken = tokens[0].Trim();
            formToken = tokens[1].Trim();
        }
    }
    AntiForgery.Validate(cookieToken, formToken);
}