1P by GN⁺ | ★ favorite | 댓글 1개
  • ruby-saml 1.17.0 이하에서 발견된 CVE-2025-25291·CVE-2025-25292는 유효 서명 하나만으로 임의 사용자 로그인을 가능하게 해 SAML SSO 환경의 계정 탈취 위험을 키움
  • 취약점은 서명 검증 경로에서 REXML과 Nokogiri가 같은 XML 문서의 서로 다른 Signature 요소를 해석할 수 있는 파서 차이에서 비롯됨
  • SignedInfo, SignatureValue, assertion 해시, DigestValue가 서로 다른 파서 결과에서 조합되면서 각 검사는 통과하지만 해시와 서명의 연결이 끊어질 수 있음
  • GitHub는 ruby-saml 재도입 검토 중 버그바운티와 Security Lab 리뷰를 진행했고, GitLab에서 악용 가능한 인스턴스를 발견해 보안팀에 알렸으며, 현재 GitHub 인증에는 ruby-saml을 쓰지 않음
  • 사용자는 ruby-saml 1.18.0으로 업데이트해야 하며, omniauth-saml처럼 ruby-saml을 참조하는 라이브러리도 수정 버전을 참조하는 릴리스로 함께 올려야 함

ruby-saml 인증 우회의 영향

  • ruby-saml 1.17.0 이하에서 중요도 높은 인증 우회 취약점 2건이 확인됨
    • CVE-2025-25291
    • CVE-2025-25292
  • 공격자는 대상 조직의 SAML response 또는 assertion 검증에 쓰이는 키로 생성된 유효 서명 하나가 있으면 직접 SAML assertion을 만들어 임의 사용자로 로그인할 수 있음
  • 가능한 서명 출처는 다음과 같음
    • 권한이 낮은 다른 사용자의 서명된 assertion 또는 response
    • 일부 경우 공개 접근 가능한 SAML IdP의 서명된 metadata
  • 이 취약점은 결과적으로 계정 탈취 공격에 활용될 수 있음
  • GitHub는 현재 인증에 ruby-saml을 사용하지 않지만, SAML 인증에 오픈소스 라이브러리를 다시 쓰는 방안을 검토하던 중 ruby-saml을 평가함
  • ruby-saml은 다른 인기 프로젝트와 제품에서도 쓰이며, GitHub는 GitLab에서 악용 가능한 인스턴스를 발견해 GitLab 보안팀에 통지함

GitHub가 다시 ruby-saml을 검토한 이유

  • GitHub는 2014년까지 ruby-saml을 사용했지만, 당시 필요한 기능이 부족해 자체 SAML 구현으로 이동함
  • 이후 자체 구현에서도 암호화된 assertion 관련 취약점인 CVE-2024-9487 같은 버그바운티 보고가 있었고, GitHub는 ruby-saml 재도입을 검토하기 시작함
  • 2024년 10월에는 ahacker1이 발견한 ruby-saml 인증 우회 취약점 CVE-2024-45409가 공개됨
  • GitHub는 ruby-saml 전환 가능성을 더 면밀히 평가하기 위해 비공개 버그바운티를 시작함
    • 선정된 연구자들에게 ruby-saml로 SAML 인증을 수행하는 GitHub 테스트 환경 접근을 제공함
    • GitHub Security Lab도 ruby-saml의 공격 표면을 함께 검토함

두 XML 파서가 만든 검증 불일치

  • 코드 리뷰 중 ahacker1과 GitHub Security Lab은 ruby-saml의 서명 검증 경로에서 두 XML 파서가 함께 쓰인다는 점을 확인함
    • REXML: 순수 Ruby로 구현된 XML 파서
    • Nokogiri: libxml2, libgumbo, JRuby용 Xerces 등을 감싼 API를 제공하며 XML과 HTML 파싱을 지원함
  • 문제가 된 경로는 xml_security.rbvalidate_signature 메서드임
  • 이 메서드는 첫 번째 Signature 요소와 실제 SignatureValue를 REXML로 읽음
    • REXML::XPath.first(@working_copy, "//ds:Signature", {"ds"=>DSIG})
  • 반면 같은 검증 흐름 안에서 SignatureSignedInfo는 Nokogiri로 다시 조회되고 정규화됨
    • document.at_xpath('//ds:Signature', 'ds' => DSIG)
    • noko_signed_info_element.canonicalize(canon_algorithm)
  • assertion은 Nokogiri로 추출·정규화·해시 처리되지만, 비교 대상인 DigestValue는 REXML에서 나옴
  • 최종 검증 재료가 다음처럼 갈라짐
    • assertion은 Nokogiri로 추출·정규화한 뒤 해시됨
    • 비교 대상 해시는 REXML이 읽은 DigestValue에서 옴
    • SignedInfo는 Nokogiri로 추출·정규화됨
    • SignatureValue는 REXML로 추출됨

SAML 서명 검증에서 끊어진 보안 연결

  • SAML response는 IdP에서 SP로 로그인 사용자 정보를 XML 형식으로 전달함
  • HTTP POST binding을 쓰면 SAML response가 사용자의 브라우저를 거쳐 SP로 이동하므로, 사용자가 메시지를 변조하지 못하게 서명 검증이 필요함
  • 단순화한 SAML response에서 중요한 정보는 보통 Assertion 내부의 SubjectNameID에 들어 있음
  • 일반적으로 assertion 또는 전체 SAML response가 서명될 수 있음
  • assertion이 서명된 경우 검증은 두 단계로 진행됨
    • Signature를 제거한 assertion을 정규화하고 해시해 DigestValue와 비교함
    • SignedInfo를 정규화하고 SignatureValue로 서명을 검증함
  • 이번 취약점에서는 두 단계가 각각 통과하더라도 서로 같은 데이터를 보장하지 못함
    • 해시는 실제 assertion의 해시일 수 있음
    • 서명은 다른 SignedInfo 요소에 대한 서명일 수 있음
  • 필요한 보안 속성은 해시된 내용, 해시, 서명이 직접 연결되는 것이며, 검증 후에는 실제 검증된 부분에서만 정보를 읽어야 함

실제 익스플로잇 구성 방식

  • 핵심 조건은 REXML과 Nokogiri가 같은 XML 문서에서 서로 다른 Signature 를 보게 만드는 것이었고, 실제로 가능했음
  • ahacker1은 버그바운티 참여 중 파서 차이를 이용한 동작 익스플로잇을 먼저 만듦
  • GitHub Security Lab은 Trail of Bits의 Ruby 퍼저 ruzzy를 이용해 다른 파서 차이 기반 익스플로잇을 만듦
  • 예시 익스플로잇은 StatusDetail 요소 안에 Nokogiri에만 보이는 추가 Signature를 넣음
  • 검증 흐름은 다음처럼 분리됨
    • Nokogiri가 보는 signature의 SignedInfo가 정규화됨
    • REXML이 보는 signature에서 추출한 SignatureValue로 검증됨
    • Nokogiri가 ID로 찾은 assertion이 정규화·해시됨
    • REXML이 읽은 DigestValue와 해시가 비교됨
  • 결국 유효한 SignedInfo와 유효한 서명이 서로 맞고, 조작된 assertion과 그 계산된 digest도 서로 맞아 ruby-saml이 assertion을 받아들임

완화책과 탐지 한계

  • Nokogiri로 SAML response를 파싱할 때 파싱 오류를 확인하면 현재 알려진 비공개 익스플로잇 일부를 막을 수 있음
  • Nokogiri 파싱 오류는 예외로 발생하지 않으므로, 파싱된 문서의 errors 멤버를 직접 확인해야 함
  • 예시 코드는 Nokogiri::XML::ParseOptions::STRICT | Nokogiri::XML::ParseOptions::NONET 옵션을 사용하고, doc.errors.any?일 때 오류를 발생시킴
  • 이 방식은 완전한 수정은 아니지만, 최소 하나의 익스플로잇을 실행 불가능하게 만듦
  • 신뢰할 수 있는 침해 지표는 알려져 있지 않음
    • 잠재적 지표 하나는 debug 유사 환경에서만 동작함
    • 이를 공개하려면 동작 익스플로잇 구현 세부사항을 너무 많이 드러내야 해 공개하지 않음
  • 권장 확인 방법은 SP 측에서 사용자의 예상 위치와 맞지 않는 IP 주소의 SAML 로그인 같은 의심스러운 로그인을 찾는 것임

수정 방향과 업데이트 대상

  • 초기 수정은 API 호환성 문제 때문에 XML 파서 하나를 제거하지 않음
  • 더 근본적인 문제는 해시 검증과 서명 검증의 분리였고, 이 분리가 파서 차이를 통해 악용 가능해짐
  • XML 파서 하나를 제거하는 작업은 다른 이유로 이미 계획되어 있었으며, 추가 개선과 함께 주요 릴리스에서 이뤄질 가능성이 있음
  • ruby-saml 사용자는 수정이 포함된 1.18.0으로 업데이트해야 함
  • ruby-saml을 사용하는 라이브러리도 함께 확인해야 함
    • 예: omniauth-saml
    • 해당 라이브러리가 수정된 ruby-saml 버전을 참조하는 버전으로 업데이트되어야 함
  • GitHub Security Lab은 향후 GitHub Security Lab repository에 개념증명 익스플로잇을 공개할 예정임

공개와 대응 일정

  • 2024-11-04: ruby-saml로 SAML 인증을 평가하던 GitHub 테스트 환경에 대해 인증 우회를 입증한 버그바운티 보고가 접수됨
  • 2024-11-04: 잠재적 완화책 식별과 테스트 작업이 시작됨
  • 2024-11-12: 첫 번째 완화 계획을 무력화하는 두 번째 인증 우회가 발견됨
  • 2024-11-13: ruby-saml maintainer Sixto Martín과 최초 접촉함
  • 2024-11-14: 두 파서 차이가 ruby-saml에 보고됐고 maintainer가 즉시 응답함
  • 2024-11-14: maintainer와 ahacker1이 잠재적 패치 작업을 시작함
    • 초기 아이디어 중 하나는 XML 파서 하나를 제거하는 것이었지만, 하위 호환성을 깨지 않고는 가능하지 않았음
  • 2025-02-04: ahacker1이 하위 호환성이 없는 수정안을 제안함
  • 2025-02-06: ahacker1이 하위 호환 가능한 수정안도 제안함
  • 2025-02-12: GitHub Security Lab advisory의 90일 기한이 종료됨
  • 2025-02-16: maintainer가 하위 호환성을 유지하고 이해하기 쉬운 수정 방향으로 작업을 시작함
  • 2025-02-17: ruby-saml 릴리스와 GitLab 온프레미스 제품 릴리스를 조율하기 위해 GitLab과 최초 접촉함
  • 2025-03-12: 수정된 ruby-saml 버전이 릴리스됨

댓글과 토론

Hacker News 의견들
  • GitHub의 SAML 구현은 쓸모가 없다고 봄
    원래 의도는 개인 계정을 엔터프라이즈에 가져와 쓰는 것인데, GitHub 사이트 안에서는 어느 정도 동작하지만, GitHub로 로그인한 앱이 조직 수준에서 승인된 뒤에는 조직 멤버십을 읽는 일을 막지 못함
    SAML 세션은 해당 앱이 그 사용자에게서 받은 토큰으로 데이터를 가져올 때만 필요하고, 실제로는 SAST 도구들이 거의 항상 앱 인스턴스 토큰을 쓰면서 조직 내 GitHub 계정만 있으면 코드를 보여줬음
    Tailscale은 알려주자 고쳤고, Sonarcloud는 아무에게도 말하지 말아 달라고 했으며, GitHub는 몇 주 뒤 “완전히 예상된 동작”이라고 답했지만, 알려준 어떤 벤더도 그렇게 이해하지 않았고 문서도 GitHub의 답과 모순됐음
    보안 버그 신고는 우연히 발견해도 보람 없는 일이고, 이걸 직업으로 하는 건 상상하기 어렵다

    • “엔터프라이즈에 개인 계정을 가져온다”는 방식은 권한 부여를 넘어선 문제도 만듦
      GitHub가 아주 가끔 업무 PR 병합 시 개인 이메일 주소를 기본값으로 써버린 적이 있어서, 누가 묻는다면 GitHub든 어디든 개인용과 업무용을 같은 계정에 섞지 말라고 조언함
    • 벤더 입장도 어느 정도 이해되는 게, GitHub가 여기서 제대로 구현하기를 극도로 어렵게 만듦
      저장소/커밋/PR 분석 도구인 https://dev.log.xyz를 만들면서 “GitHub에서 볼 수 있는 것만 Devlog에서도 볼 수 있다”를 맞추는 데 큰 노력이 들었고, 전체 경험이 매우 답답했음
      GitHub의 OAuth 권한 선택기도 지나치게 혼란스러워서, “GitHub로 로그인”할 때 어떤 조직의 어떤 정보를 공유하는지 확신하기 어렵다
    • 이런 운영 방식은 모든 층위에서 반복됨
      JavaScript에 익숙한 젊은 개발자에게 클라이언트 측 검증만으로는 충분하지 않다고 설득하는 것도 어렵고, 기능 요구사항과 예산을 정하는 사업자에게는 더더욱 어렵다
    • 공유 자격 증명에서 오는 편의와 SSO로 보안이 좋아질 수 있다는 전제는 이런 취약점 하나로 무너짐
      어떤 형태의 비밀번호 저장소든, 심지어 물리적인 저장소나 비밀번호 재사용조차 결과적으로 더 안전해질 수 있음
      결국 미니멀리즘이 또 이긴 셈
    • 위 내용을 이번 취약점 연구와 연결해 보려는 사람에게는, 이건 관련 없어 보임
      “GitHub doesn’t currently use ruby-saml for authentication, but began evaluating the use of the library with the intention of using an open source library for SAML authentication once more”라고 되어 있음
  • 최근 SAML 구현을 해야 했는데, 이 제목이 전혀 놀랍지 않음
    SAML 명세 자체는 꽤 합리적이지만, 그 기반인 XML 서명, 더 나아가 XML 정규화는 표준이라고 부를 수 있다면 정말 미친 수준임
    이런 뒤틀리고 타락한 명세는 위원회만이 만들 수 있고, 한 개인의 머리로는 이렇게 모순적인 생각들을 붙잡고 결합할 수 없었을 것 같음
    서명을 대역 외로 전송하기만 해도 SAML은 구현하기 즐거운 물건이 됐을 것임

    • 말한 것보다 훨씬 더 나쁨
      XML은 말 그대로 eXtensible Markup Language인데, SAML 표준화 위원회는 그 위에 자기들만의 확장 메커니즘 언어를 또 발명했음
      인증 쿠키에 들어갈 정보와 크게 다르지 않은 아주 작은 데이터를 위해 프로토콜 위에 또 프로토콜을 올리는 건, 가장 크고 관료적인 위원회만 만들어낼 수 있는 특수한 종류의 어리석음임
    • SSO가 애초에 구제 가능하긴 한지 모르겠음
      서로 다른 계정에 각각 로그인하는 발상 자체는 괜찮아 보임
      계정들을 서로 연결해 한 번에 대규모로 털릴 수 있게 만드는 구조는 근본적으로 더 위험하다
  • SAML, 더 넓게는 XML-DSIG는 널리 쓰이는 보안 프로토콜 중 문자 그대로 최악이라고 봄
    일반적으로 어떤 비용을 치르더라도 OAuth로 전환해야 하고, 적어도 새 제품을 시장에 내면서 이것에 의존하는 일은 거부할 것임
    매우 위험하며, 실용적인 형식 검증에서 돌파구가 나오지 않는 한 이번이 마지막 DSIG 취약점이거나 최악의 취약점일 거라고 상상하기 어렵다

    • 언젠가 XML DSig가 하는 믿을 수 없이 멍청한 일들을 모두 글로 쓸 것임
      암호학은 건드리지도 않았는데도 이미 엔터프라이즈 소프트웨어식 사고의 정점임
      누군가는 WS-*와 OASIS/XACML 같은 것들의 메일링 리스트와 표준화 기구의 참상을 깊이 파야 함
    • Security Cryptography Whatever가 이번 주 SAML 난장판을 어떻게 다룰지 재미있을 것 같음
    • 내가 이해한 게 맞다면, SAML에는 SSO 제공자가 세션을 취소할 수 있다는 차별점이 아직 남아 있음
      그게 맞나?
  • 으, 다른 선택지가 없는 경우가 아니라면 아무도 REXML을 쓰면 안 됨
    잘못된 XML도 기꺼이 파싱해 버려서 이후 단계에 무한히 많은 문제를 일으킴
    말 그대로 정규 표현식으로 XML을 파싱하고 있고, 왜 그러면 안 되는지 보여주는 훌륭한 사례임
    프로젝트들이 성능 때문에 Nokogiri를 쓰기 시작한 게 아니라, 정확성 때문에 쓴 것임

    • 교과서적인 예시 아닌가?
      정규 언어가 아닌 것을 정규 표현식으로 파싱하지 말라는 것
    • AI 코드 보조의 위험 중 하나는 대형 코드베이스에서 쓰는 라이브러리의 더 넓은 맥락을 꼭 보지는 않는다는 점임
      최근 o3를 테스트했는데, 특정 코드 블록의 라이브러리와 무관한 문제를 고치려 할 때마다 그 블록이 쓰는 라이브러리를 계속 바꿨음
      Sonnet에서는 그런 현상을 보지 못했음
      이렇게 수정 과정에서 코드베이스나 표준 라이브러리에 이미 있어서 테스트는 통과하고 Gemfile 변경도 필요 없는 더 열등한 라이브러리/gem으로 바뀌면서 문제가 스며들기 쉬워 보임
  • SAML은 설계상 안전하지 않음
    예전부터 더 잘 설명한 글들이 있고, 예를 들면 https://joonas.fi/2021/08/saml-is-insecure-by-design/가 있음
    여기 오래된 스레드에서 기억에 남은 문구는 “의미가 아니라 바이트에 서명하라”였음
    파서 간 차이는 예상 가능하고 때로는 필연적임
    서명된 응답에서 무엇을 얻으려는지가 매우 중요함
    현대 TLS의 딜레마 중 하나는 때로는 내부 CA 하나를 신뢰하고 싶고, 그건 쉬운 경로지만, 파트너의 CA 인증서를 받아들이고 파트너가 여러 곳이면 더 이상 최종 인증서만 볼 수 없고 체인의 루트도 의사결정에서 똑같이 중요해진다는 점임
    그래서 가능하면 AWS 서명 알고리즘도 피하라고 권함
    V4는 이론적으로 안전하지만 AWS는 두 번 망쳤고, SigV1과 SigV3는 설계상 안전하지 않았는데도 어찌 됐든 설계 검토를 통과해 공개로 나갔음

  • 훌륭한 글임
    글에서도 언급되지만, ahacker1에게 크게 박수쳐야 함
    SAML 구현을 안전하게 만들기 위해 매우 정교하고 가치 있는 일을 하고 있고, SSOReady도 그의 작업에 정말 감사하고 있음
    이번 주 초 WorkOS도 ahacker1과의 협업에 대해 좋은 글을 올렸음: https://workos.com/blog/samlstorm

  • “GitLab에서 이 취약점을 악용 가능한 사례로 발견했고 보안팀에 알렸다”는 부분이 있는데, 궁금한 사람들을 위해 말하면 GitLab은 수정판을 이미 냈음
    https://about.gitlab.com/releases/2025/03/12/patch-release-g...

  • 관련 글로 Latacora의 2019년 글, How (not) to sign a JSON object[1]가 있음
    요약하면, 트리 구조를 중첩해 서명하는 일은 어렵고 함정이 많음
    봉투가 메시지를 원시 문자열로 담고, 그 원시 문자열에 서명하는 편이 더 쉽다
    [1]: https://www.latacora.com/blog/2019/07/24/how-not-to/

  • 여기서 더 단순한 결론은 서명이 있어야 하는 위치에서 서명을 찾으라는 것 아닌가?
    “//ds:Signature”처럼 지나치게 일반적인 XPath를 써서 예상치 못한 위치의 아무 서명이나 찾게 하지 말고

    • 취약점에 대한 대응은 대개 너무 느슨하다고 느낌
      위험한 구성요소만 외과적으로 제거하는 게 아니라, 목욕물과 함께 아이도 조금 버려야 하고, 대량으로 잘라내고 항암치료하듯 처리해야 함
      자부심 있는 IT 관리자라면 앞으로의 계획에서 SAML은 제외해야 함
      SSO라는 발상 전체도 의심스럽고, XML 파싱은 일주일에 두 번 맞았으니 앞으로 피해야 함
      XML을 JSON으로 대체하는 정책에 뭐가 문제가 있나?
    • 좀 과격하게 말하면, 결론은 큰 몽둥이를 들고 웹 개발자들이 보안 민감 코드 근처에 손대지 못하게 해야 한다는 것임
      설계, 프로토콜, 데이터 형식부터 그래야 함
      일반적인 웹 개발의 습관과 설계 고려사항은 보안 코드에 필요한 것과 맞지 않고, 정상적인 코드를 쓰는 데 필요한 것과도 종종 정반대임
  • 블로그 글에서 취약점을 설명하면서 정작 문제가 된 파서 차이를 빼놓은 건 좀 짜증남
    이야기의 도입부를 써놓고 절정을 생략한 것과 같음

    • 형제 댓글의 블로그 글 <https://news.ycombinator.com/item?id=43374972>에 관련 세부사항이 있었음
      (...//ds:DigestValue).firstChild.nodeValue를 하면서 .firstChild가 Node인지 확인하지 않았고, 문제가 된 경우에는 Comment였음
      그래서 정규화되지 않은 쪽은 “가려진” 서명을 봤고, 주석을 버린 수정된 쪽은 Node를 봤으며, 두 구현이 서명된 문서에 대해 다르게 판단하면 웃지 못할 일이 벌어짐
    • 전 세계 수많은 시스템에서 권한 우회를 가능하게 하는 제로데이를, 해당 시스템 담당자들이 고칠 기회를 갖기 전에 직접 공개한 책임을 지고 싶지 않았던 것 같음
      구체적인 내용은 조만간 나오겠지