CI ID Token Manipulated claims vulnerability for forked projects MR pipelines
Related issue
HackerOne report #3137660 by skybound on 2025-05-09, assigned to @greg:
Report | Attachments | How To Reproduce
Report
Summary
An attacker can trick GitLab into issuing, and subsequently using, an ID token for a protected branch of a repository which they do not have push or merge permissions for through the use of forks and merge request pipelines. This could be used to authenticate with other services such as AWS, gaining access to resources where access is restricted through the protected branches of the repository.
Steps to reproduce
- Create a
victimproject with amainprotected branch - Configure the repository to only allow maintainers to push/merge into the
mainprotected branch - Commit the
first-gitlab-ci.yml() as
.gitlab-ci.ymlinto thevictimproject directly to themainbranch - In the resultant pipeline, note the
subclaim - Add a second user as a developer to the project
The following steps are to be taken as the second user:
- Fork the
victimrepository, the fork will be referenced as theattackerrepository - Run the pipeline manually within the
attackerrepository - In the resultant pipeline, note the
subclaim - it should be different to the previoussubclaim - Commit the
second-gitlab-ci.yml() to the
attackerrepository as.gitlab-ci.yml(replacing the previous version) directly to themainbranch - Submit a merge request to the
victimrepository, attempting to mergemainfromattackerintomain - Note a new pipeline is triggered for the merge request
- View the
subclaim in this pipeline. Note it has the same value as the pipeline from step 4.
Impact
This allows an attacker that has low privileges within a repository to get JWT / ID tokens from branches they may not have push/merge access to. ID tokens are commonly used to authenticate with other services, for example authenticating to AWS via OIDC. The following assumes the third party service is AWS, however similar applies to other services such as Azure. However, this post is limited to AWS for brevity.
Access to AWS is constrained through AWS trust relationships which check the sub claim which by default validates the repository name and branch (as described in https://docs.gitlab.com/ci/cloud_services/aws/#configure-a-role-and-trust). For example, the sub field would be project_path:mygroup/myproject:ref_type:branch:ref:main if the pipeline was run from the main branch in the project at mygroup/myproject.
An attacker through their fork could modify .gitlab-ci.yml to extract the ID token for the main branch of the victim repository. This would allow them to authenticate to third party services as if they were the main branch of the victim repository. If the repository hosted Terraform code that was deployed automatically into an AWS account through CI/CD with trust relationships configured to only allow the main branch of the victim repository to access an administrative deployment role, an attacker would directly gain administrative access to the AWS account.
Examples
victim repository = https://gitlab.com/Skybound/oidc\
pipeline from step 4 = https://gitlab.com/Skybound/oidc/-/jobs/9990314446\
attacker repository = https://gitlab.com/rs_mohit/oidc\
pipeline from step 7 = https://gitlab.com/rs_mohit/oidc/-/jobs/9990324982\
pipeline from step 12 = https://gitlab.com/Skybound/oidc/-/jobs/9990328384
What is the current bug behavior?
The sub field from the merge request pipeline is the same as if the pipeline was directly run from the main branch.
What is the expected correct behavior?
The sub field from the merge request pipeline should not be the same as the value when running the pipeline directly against main.
Relevant logs and/or screenshots
The JWT token claims when running against the main branch of the victim repository:
{
"namespace_id": "[REDACTED]",
"namespace_path": "[REDACTED]",
"project_id": "[REDACTED]",
"project_path": "[REDACTED]/oidc",
"user_id": "[REDACTED]",
"user_login": "Skybound",
"user_email": [REDACTED],
"user_access_level": "owner",
"pipeline_id": "1809987654",
"pipeline_source": "push",
"job_id": "9990314446",
"ref": "main",
"ref_type": "branch",
"ref_path": "refs/heads/main",
"ref_protected": "true",
"runner_id": 32976687,
"runner_environment": "gitlab-hosted",
"sha": "[REDACTED]",
"project_visibility": "private",
"ci_config_ref_uri": "gitlab.com/[REDACTED]/oidc//.gitlab-ci.yml@refs/heads/main",
"ci_config_sha": "[REDACTED]",
"jti": "[REDACTED]",
"iat": 1746820333,
"nbf": 1746820328,
"exp": 1746823933,
"iss": "https://gitlab.com",
"sub": "project_path:[REDACTED]/oidc:ref_type:branch:ref:main",
"aud": "sts.amazonaws.com"
}
The JWT token claims when running against the main branch of the attacker repository:
{
"namespace_id": "[REDACTED]",
"namespace_path": "[REDACTED]",
"project_id": "[REDACTED]",
"project_path": "[REDACTED]/oidc",
"user_id": "[REDACTED]",
"user_login": "[REDACTED]",
"user_email": [REDACTED],
"user_access_level": "owner",
"pipeline_id": "1809988925",
"pipeline_source": "web",
"job_id": "9990324982",
"ref": "main",
"ref_type": "branch",
"ref_path": "refs/heads/main",
"ref_protected": "true",
"runner_id": 12270831,
"runner_environment": "gitlab-hosted",
"sha": "[REDACTED]",
"project_visibility": "private",
"ci_config_ref_uri": "gitlab.com/[REDACTED]/oidc//.gitlab-ci.yml@refs/heads/main",
"ci_config_sha": "[REDACTED]",
"jti": "[REDACTED]",
"iat": 1746820450,
"nbf": 1746820445,
"exp": 1746824050,
"iss": "https://gitlab.com",
"sub": "project_path:[REDACTED]/oidc:ref_type:branch:ref:main",
"aud": "sts.amazonaws.com"
}
The JWT token claims when running the main branch of the attacker repository as a merge request pipeline within the victim repository:
{
"namespace_id": "[REDACTED]",
"namespace_path": "[REDACTED]",
"project_id": "[REDACTED]",
"project_path": "[REDACTED]/oidc",
"user_id": "[REDACTED]",
"user_login": "[REDACTED]",
"user_email": [REDACTED],
"user_access_level": "developer",
"pipeline_id": "1809989379",
"pipeline_source": "merge_request_event",
"job_id": "9990328384",
"ref": "main",
"ref_type": "branch",
"ref_path": "refs/heads/main",
"ref_protected": "false",
"runner_id": 12270831,
"runner_environment": "gitlab-hosted",
"sha": "[REDACTED]",
"project_visibility": "private",
"ci_config_ref_uri": "gitlab.com/[REDACTED]/oidc//.gitlab-ci.yml@refs/heads/main",
"ci_config_sha": "[REDACTED]",
"jti": "[REDACTED]",
"iat": 1746820490,
"nbf": 1746820485,
"exp": 1746824090,
"iss": "https://gitlab.com",
"sub": "project_path:[REDACTED]/oidc:ref_type:branch:ref:main",
"aud": "sts.amazonaws.com"
}
Branch Protection Rules showing maintainer only:
Output of checks
This bug happens on GitLab.com and self-managed instances
Impact
An attacker can leverage this issue to obtain ID tokens for protected branches of repositories. This could allow them to authenticate and access third party services that trust GitLab as an OIDC provider. For example, administrative access to cloud accounts.
Attachments
Warning: Attachments received through HackerOne, please exercise caution!
How To Reproduce
Please add reproducibility information to this section:
