IDOR in "external status check" API leaks data about any status check on the instance
HackerOne report #1372216 by joaxcar
on 2021-10-16, assigned to @nmalcolm:
Report
Summary
The API endpoint for returning approval from an external status check
contains an IDOR that lets a user list information about all external status checks
on the GitLab instance. The feature is an Ultimate
feature, but can be accessed by starting an Ultimate
trial on GitLab.com. So the attack is possible with a regular account.
When an external status check
is configured, the project will send out information about MRs to a specified endpoint. This endpoint can then be configured to answer to the request to "pass" the status check. Description of the feature here. The API endpoint for this answer is (info)
POST /projects/:id/merge_requests/:merge_request_iid/status_check_responses
with the passed in parameters sha
and external_status_check_id
. It is the later one that contains the IDOR. This parameter tells GitLab which external status check
the request is targeting. And the answer back from GitLab is JSON containing info about the MR but also info about the status check configuration. By altering the ID sent, a user can obtain info about any status check on the instance (even from Private projects).
Leaked information about a status check could look like the example below and could contain:
- Private project name and ID
- Name of status check
- Private address to external status check tool
- Name and ID of protected branch connected to the status check
- Access rules to protected branch, if configured also name of user that is allowed to access
"external_status_check": {
"id": 10,
"name": "Name of status check",
"project_id": 33,
"external_url": "https://victim.service.com",
"protected_branches": [
{
"id": 24,
"name": "Name of protected branch",
"push_access_levels": [
{
"access_level": 40,
"access_level_description": "Name of user with access to protected branch",
"user_id": 2,
"group_id": null
},
{
"access_level": 30,
"access_level_description": "Developers + Maintainers",
"user_id": null,
"group_id": null
}
],
"merge_access_levels": [
{
"access_level": 30,
"access_level_description": "Developers + Maintainers",
"user_id": null,
"group_id": null
},
{
"access_level": 40,
"access_level_description": "Name of user with access",
"user_id": 2,
"group_id": null
}
],
"allow_force_push": true,
"unprotect_access_levels": [],
"code_owner_approval_required": true
}
Steps to reproduce
- Create two users
victim01
andattacker01
- Log in as
victim01
and create a new PRIVATE project calledvictim_project
at https://gitlab.domain.com/projects/new#blank_project - Go to go to project settings https://gitlab.domain.com/victim01/victim_project/edit and expand "Merge request" under "General". Scroll down to "Status checks" and click create new.
- Name the status check "Victim status check" and enter a API endpoint "https://victim.hidden.com"
- Click save
- Log out and log back in as
attacker01
- Go through step 2 to 5 again but name the project
attacker_project
and the status check anything. Take a note of the ID of the project. We will call itattackID
- Now create a new branch in
attacker_project
at https://gitlab.domain.com/attacker01/attacker_project/-/branches/new - Click "Create new merge request" when the branch is created. Name the MR anything and click create
- Go to https://gitlab.domain.com/-/profile/personal_access_tokens and create an access token for
attacker01
, we will call itTOKEN
- Open a terminal and make this request (the SHA is just "a" here, we will get the correct one as a response)
curl --request POST \
--url 'https://gitlab.domain.com/api/v4/projects/<ATTACKID>/merge_requests/1/status_check_responses?sha=a&external_status_check_id=2' \
--header 'Authorization: Bearer <TOKEN>'
- This iniitial request will return an error saying
the correct sha is <SHA>
- Now send with correct SHA
curl --request POST \
--url 'https://gitlab.domain.com/api/v4/projects/<ATTACKID>/merge_requests/1/status_check_responses?sha=<SHA>&external_status_check_id=2' \
--header 'Authorization: Bearer <TOKEN>'
- We will get a response with info about the MR and in the end info about the
status check
- Now send the same request again but change status check id to 1
curl --request POST \
--url 'https://gitlab.domain.com/api/v4/projects/<ATTACKID>/merge_requests/1/status_check_responses?sha=<SHA>&external_status_check_id=1' \
--header 'Authorization: Bearer <TOKEN>'
- The same MR info is returned, but in the end there will be info about the private status check from
victim_project
Impact
An attacker can leak information about any external status check
on the instance. This data contains sensitive private information.
What is the current bug behavior?
The parameter external_status_check_id
is not restricted to the status checks configured in the project.
What is the expected correct behavior?
The parameter should only allow IDs that corresponds to status checks in the target project.
Output of checks
This bug happens on GitLab.com
Impact
Leaked information about a status check could look like the example below and could contain:
- Private project name and ID
- Name of status check
- Private address to external status check tool
- Name and ID of protected branch connected to the status check
- Access rules to protected branch, if configured also name of user that is allowed to access
How To Reproduce
See above!