June 17, 2026
Unauthenticated IDOR on NASA’s GitLab Instance — From Recon to Bypass
Target: gitlab.smce.nasa.gov Program: NASA VDP (Bugcrowd) Severity: P3 — Broken Access Control / IDOR Status: Resolved
Ghaddarittoo
4 min read
The Wrong Assumption That Protects Bad Configs
When a bug hunter sees a third-party hosted platform — GitLab, Jira, Confluence, whatever — the instinct is to move on. "It's managed software, it's already been hardened by the vendor." That instinct is wrong, and this finding is a good example of why.
The security posture of any self-hosted platform is not decided by the vendor. It's decided by whoever deployed it and how they configured it. GitLab ships with sane defaults and a lot of flexibility, but that flexibility cuts both ways. A single misconfigured setting or an overlooked API permission can expose the entire instance — and in NASA's case, that misconfiguration led to unauthenticated access to internal user data on a system explicitly marked as private.
The lesson: when you see a self-hosted third-party app, don't look at the product, look at the config. That's where the bugs live.
Discovery
The target was gitlab.smce.nasa.gov, the self-hosted GitLab instance for NASA's SMC Engineering team. The instance was configured as restricted — meaning it's supposed to be completely inaccessible to unauthenticated users. No public repos, no browsable UI, no API access.
The first confirmation of that came from hitting the base users API directly:
curl -i "https://gitlab.smce.nasa.gov/api/v4/users"
HTTP/2 403 Forbiddencurl -i "https://gitlab.smce.nasa.gov/api/v4/users"
HTTP/2 403 ForbiddenGood. Access control is enforced. The instance knows it's private.
Except — it wasn't enforcing it consistently.
The Bug: Filter Parameter Bypass
GitLab's REST API supports query filters on the /users endpoint. One of them is ?username=, which scopes the response to a single user by their username. The developers who locked down the instance blocked the base /api/v4/users call, but they didn't account for the filtered variant.
curl -i "https://gitlab.smce.nasa.gov/api/v4/users?username=ANY_USERNAME"curl -i "https://gitlab.smce.nasa.gov/api/v4/users?username=ANY_USERNAME"Response:
[
{
"id": [REDACTED],
"username": "[REDACTED]",
"name": "[REDACTED]",
"state": "active"
}
][
{
"id": [REDACTED],
"username": "[REDACTED]",
"name": "[REDACTED]",
"state": "active"
}
]The 403 was gone. Account existence confirmed, internal user ID exposed. The access control was applied at the route level, not at the authorization layer — meaning any variation of the request that didn't match the exact blocked pattern would fall through cleanly.
Usernames to feed into that filter were trivially available from the public group members page:
https://gitlab.smce.nasa.gov/groups/heliocloud/-/group_membershttps://gitlab.smce.nasa.gov/groups/heliocloud/-/group_membersNo authentication required to view it. Members are listed in the format @username — open in a browser, no login, full list. That's the starting point for enumeration.
Chaining to SSH Key Metadata
Having a user ID is one thing. What made this chain meaningful was what you could pull once you had one.
GitLab exposes an endpoint for retrieving a user's public SSH keys:
/api/v4/users/:id/keys/api/v4/users/:id/keysOn a properly restricted instance, this should require authentication. It didn't.
curl -i "https://gitlab.smce.nasa.gov/api/v4/users/[REDACTED_ID]/keys"
[
{
"id": [REDACTED],
"title": "[REDACTED — internal workstation hostname]",
"key": "ssh-ed25519 [REDACTED]"
},
{
"id": [REDACTED],
"title": "[REDACTED — contractor email / internal hostname]",
"key": "ssh-ed25519 [REDACTED]"
}
]curl -i "https://gitlab.smce.nasa.gov/api/v4/users/[REDACTED_ID]/keys"
[
{
"id": [REDACTED],
"title": "[REDACTED — internal workstation hostname]",
"key": "ssh-ed25519 [REDACTED]"
},
{
"id": [REDACTED],
"title": "[REDACTED — contractor email / internal hostname]",
"key": "ssh-ed25519 [REDACTED]"
}
]The title field is user-defined — developers typically set it to whatever machine the key lives on. In practice this means it contains things like:
- Workstation asset tags — internal IT naming conventions that map to physical machines
- Internal development server names — hostnames of live infrastructure
- Fully Qualified Domain Names pointing into internal data center infrastructure — exact network paths to production systems
- Contractor email addresses — identifying external contractors and their affiliated organizations
The internal FQDNs were particularly notable. That's not just a machine name — it's the absolute network path to an asset inside a secured data center. For an attacker, that skips the entire discovery phase of an engagement and drops them directly into targeted reconnaissance on high-value infrastructure.
Impact
The 403 on the base endpoint was the signal that NASA intended this data to be private. The bypass broke that intent entirely.
An unauthenticated attacker could:
- Harvest usernames from the publicly accessible group members page
- Enumerate all accounts on the instance via the
?username=filter, collecting internal user IDs - Pull SSH key metadata for every enumerated user via
/users/:id/keys - Build a map of internal workstation names, server hostnames, internal FQDNs, and contractor affiliations, all without ever authenticating
None of this leaves a meaningful audit trail on the application side. It's passive, silent reconnaissance using only legitimate API endpoints.
The Fix — And Why It Failed
After the report was triaged and validated, NASA's team deployed a fix. The approach they went with was a Cloudflare WAF rule that blocked requests containing /api/v4/users in the path.
It's the kind of fix you reach for when you want something quick. Drop a static string-match rule at the edge, block the pattern, done.
The problem is that a static WAF rule that pattern-matches on a literal string is only as strong as the completeness of that string. And strings can be encoded.
The bypass:
/api/v4/user%73?username=ANY_USERNAME/api/v4/user%73?username=ANY_USERNAMEThe letter s in users is URL-encoded as %73. To the WAF rule, that string doesn't contain /api/v4/users — it contains /api/v4/user%73, which is a different pattern entirely. The rule doesn't fire. The request reaches the origin server, which decodes %73 back to s before processing, and serves the response exactly as before.
curl -i "https://gitlab.smce.nasa.gov/api/v4/user%73?username=ANY_USERNAME"curl -i "https://gitlab.smce.nasa.gov/api/v4/user%73?username=ANY_USERNAME"Same response. Same data. WAF bypassed.
The same encoding trick worked on the /keys endpoint:
/api/v4/user%73/[USER_ID]/keys/api/v4/user%73/[USER_ID]/keysThis is a textbook case of WAF misplacement. You cannot fix an authorization vulnerability with a WAF rule because WAF rules operate on request strings, not on application authorization logic. The underlying issue that the /users endpoint applies access control inconsistently depending on query parameters exists in the application. A WAF rule sitting in front of it doesn't fix the application; it just adds a filter that any percent-encoded request walks straight through.
The correct fix is applied at the authorization layer: the GitLab instance's visibility setting needs to enforce authentication on all API endpoints uniformly, regardless of query parameters or path variations. That's an application-level control that percent encoding cannot bypass.
NASA's team deployed the proper fix on the second round, and the bypass was confirmed as no longer reproducible.
Key Takeaways
Self-hosted ≠ secure by default. The vendor can ship a perfectly capable access control system. If the operator misconfigures it, the access control doesn't exist. Always probe self-hosted instances as if you're looking at a blank canvas — assume nothing is locked down until you verify it is.
WAF rules are not authorization. A string-match rule at the edge is not a substitute for fixing the authorization logic in the application. Percent encoding, double encoding, case variation, and path normalization differences are all trivial ways around pattern-matching rules. The fix belongs in the layer where access decisions are actually made.
Consistent authorization matters. The base endpoint returning 403 gave a false sense of security. The /users?username= variant and the /users/:id/keys endpoint were never covered by the same control. Authorization needs to be enforced uniformly across every variation of a route — filtered, unfiltered, with path parameters, without — not just on the most obvious form of the request.
Chaining low-severity leaks. Neither the username enumeration nor the key metadata leak is catastrophic on its own. Together, they produce actionable intelligence: confirmed account existence, internal IDs, machine names, server hostnames, and contractor identities. The chain is what makes it worth reporting.