CSRF is dying

CSRF는 XSS, SQL Injection과 함께 웹에서 가장 대표적이 취약점 중 하나입니다. 현재까지도 종종 발견되는 취약점이지만, 업계에서는 오래전부터 서서히 죽어간다는 표현을 하고 있습니다.

제 생각해도 CSRF의 남은 수명이 길지는 않아 보이는데, 오늘은 무엇이 CSRF의 수명을 줄였는지 그리고 앞으로 어떤것들에 더 집중해야 하는지 이야기하려고 합니다.

What is CSRF

CSRF는 Cross-Site Request Forgery 의 약자로 쿠키의 동작 방식을 이용한 공격 방법으로 사용자의 세션 쿠키 또는 다른 인증정보를 이용하여 사용자 모르게 공격자가 의도한 서비스 요청을 처리하는 공격 방법을 의미합니다. 대표적으로 아래와 같은 액션들이 가능하죠.

  • 공격코드가 삽입된 페이지에 접근 시 피해자 모르게 패스워드 변경
  • 공격코드가 삽입된 페이지에 관리자 계정 접근 시 원격 명령 실행
  • 사용자 모르게 글 작성, 댓글 작성
  • 등 여러가지

공격에 대한 자세한 내용은 아래 문서를 참고해주세요.

Cullinan > CSRF

CSRF is Dying ☠️

현재의 CSRF는 개발자의 실수에서 비롯되지만, 그 전에 CSRF는 웹이란 기술이 가진 스펙상의 문제이기도 합니다. 그래서 구글이나 Mozila 등에선 이를 완화하기 위한 방법들을 계속 연구하였고, 보통 알고 있는 대응방안인 Anti-CSRF Token이나 Referer 등의 헤더 검증 뿐만 아니라 브라우저단에서의 대응 또한 동시에 이루어지고 있었습니다.

대표적인게 바로 SameSite Cookie 였죠.

그래서 현재는 CSRF의 성공 조건이 굉장히 복잡해졌습니다. 특히 전통적인 보안 로직이 없더라도 SameSite 등의 쿠키 설정으로 인해 공격이 실패하는 경우가 많아졌습니다.

  • Anti-CSRF Token이 존재하지 않는가?
  • Referer 헤더를 검증하지 않는가?
  • Origin 헤더를 검증하는가?
  • 주요 인증 쿠키의 SameSite 설정이 None으로 직접 지정됬는가?
  • CSP 등으로 통제되진 않는가?

이 모든게 취약하다면 CSRF가 발견될 가능성이 존재함 Web

그래서 CSRF가 죽어가고 있습니다.

가장 큰 역할을 한건 SameSite Cookie의 Default 값이 Lax로 변경된 사건인 것 같네요. 해당 내용이 각 브라우저사에 적용된 이후로는 CSRF가 예전만 못한건 사실입니다. SameSite Cookie에 대해서 간략히 이야기해보면 Site(TLD+1)를 단위로 쿠키의 Scope를 정할 수 있는데, 이 정책이 기존에는 A → B Site간의 요청 시 B 사이트의 쿠키를 포함했다면, 지금은 별도로 설정하징 않는 이상 포함되지 않는 구조입니다.

Set-Cookie: flavor=choco; SameSite=Lax

그래서 사용자의 쿠키와 세션을 이용한 CSRF 공격은 현재 브라우저에선 성공률이 많이 낮아졌습니다. SameSite에 대한 자세한 내용은 아래 글을 참고해주세요.

SameSite=Lax가 Default로? SameSite Cookie에 대해 정확하게 알아보기

MSA

MSA(MicroService Architecture) 구조도 CSRF를 줄이는데 하나의 역할을 했습니다. 전통적인 웹앱의 경우 FE/BE가 분리되지 않은 구성이 많았다면 현재는 MSA 구조가 압도적으로 많습니다. 이는 Cookie 기반의 인증보다 Header 기반의 인증이 더 많이 사용되도록 이끌어졌고, 의도하진 않았겠지만 결과적으로 CSRF의 감소를 유도한건 사실입니다. (물론 덕분에 JSON, JSONP Hijacking 이 더 많아지게 되었지만요 🤷🏽‍♀️)

OWASP TOP 10

CSRF 또한 OWASP TOP 10의 고정 멤버였다가 2017에서 빠지게 되었었습니다. 당시에는 솔직히 살짝 이해가되지 않았던 결정이였는데, 이후 행보를 보니 빠지는게 맞았었네요.

https://www.hahwul.com/cullinan/history-of-owasp-top-10/

Pass? No 🙅🏽

하지만 여전히 일부 개발/서비스는 편의성 또는 스펙 등의 이유로 SameSite=None 등으로 설정하여 CSRF에 노출될 수 있습니다. 비중을 줄이되 완전히 포기하지 않아야 하는 이유이기도 하죠. 그리고 실제로 발생 가능성이 줄어든거지, CSRF가 발생하는 경우의 리스크는 과거와 크게 다르지 않습니다.

그리고 Cookie Poisoning나 CRLF Injection 같이 쿠키 설정 구간을 통제할 수 있는 경우엔 동일하게 SameSite=None 처리로 CSRF의 영향도를 만들 수 있으니 참고는 하고 있어야합니다.

Testing Methods

그래서 개인적으로는 CSRF를 이제 좀 더 영리하게 테스트 해야할 필요가 있다고 생각합니다. 모든 Endpoint에 하나하나 테스트하기에는 실제 성공률이 낮아지다 보니 꼼꼼하게 구성한 Passive Rule을 통해서 식별하고, 문제가 있다면 중점적으로 보는게 가장 좋을 것 같습니다.

그래서 매번 테스트마다 주요 세션을 식별하고, 해당 세션에 SameSite 설정과 부가적인 방어로직을 보면서 CSRF를 중점적으로 볼지 결정이 필요할 것 같네요. 개인적으로는 SameSite=None에 따로 식별해서 높은 우선순위로 체크합니다.

위와 같이 Burp, ZAP 등에선 Passive Scan 등으로 탐지할 수 있고, 개인적으론 Risk를 조정(Medium -> High)해서 썼었습니다. 다만 종종 못잡는 경우가 있어서 따로 스크립팅해서 사용합니다.

// CookieSameSiteNone.js
// Cookie SameSite=None Check 
// by @hahwul

function scan(ps, msg, src) 
{

    var alertRisk = 3
    var alertConfidence = 2
    var alertTitle = "Cookie set with SameSite=None"
    var alertDesc = "A cookie has been set with the SameSite=None"
    var alertSolution = "Session cookies should not use SameSite =None as much as possible."

    var cweId = 0
    var wascId = 13

    var url = msg.getRequestHeader().getURI().toString();
    var headers = msg.getResponseHeader().getHeaders("Set-Cookie")
    
    if (headers != null)
    {
        var re_noneflag = /([Ss][Aa][Mm][Ee][Ss][Ii][Tt][Ee][=][Nn][Oo][Nn][Ee])/g
        if ((re_noneflag.test(headers)))
        {
            ps.raiseAlert(alertRisk, alertConfidence, alertTitle, alertDesc, url, '', '', '', alertSolution,headers, cweId, wascId, msg);
        }
    }
    
}

Conclusion

새로운 기법, 취약점, 공격 방법들이 꾸준히 나오고 있어서 누적될수록 우리가 테스팅해야할 부분들이 정말 많아지는 것 같습니다. 자동화도 중요하겠지만 개개인의 Testing Methodology가 정말 중요해지는 것 같네요.

버그를 찾아야하는 입장에선 이렇게 사라지는 공격들은 마음 아프기도 하지만, 새롭게 나오는 것들을 따라가기 위한 여유를 만들어주기도 해서 반갑기도 합니다. 그래도 시간이 좀 지난 후에 누군가가 다시 유행시킬 수도 있으니 머릿속에서 잊지는 않는게 좋겠네요 :D

대표적인 예시가 HRS(HTTP Request Smuggling)입니다. 과거에 잠깐 반짝했다가 2019년에 다시 재조명 받았듯이 CSRF도 역사속으로 사라졌다가 다시 등장할 수도 있겠죠 😉