glsec

Rules / GL081

GL081 OIDC id_tokens issued on an unrestricted pipeline source

Severity: warn
OWASP: CICD-SEC-5 – Insufficient PBAC
CWE: CWE-284 – Improper Access Control
Minimum GitLab version: 15.7 (id_tokens:)

Risk

A job that declares id_tokens: mints an OIDC JWT that federates into a cloud provider or secrets manager (AWS STS, GCP, Vault, …). If nothing restricts which pipeline sources run that job, any Developer can trigger it:

  1. Same-project MR and branch pipelines. Anyone with the Developer role can push a branch, open a merge request, and cause the job to mint a token. Cloud-side trust policies overwhelmingly condition on project_path / project_id only — in which case a token minted from an unreviewed feature branch is indistinguishable from one minted on the default branch. Effectively every Developer holds whatever role the job can assume, without ever touching a protected branch.
  2. ci_allow_fork_pipelines_to_run_in_parent_project. A parent-project member can run a fork MR's pipeline in the parent context, where fork-controlled code runs before merge.

(The naive "a fork MR steals your token" framing is not the main path: fork MR pipelines run in the fork project by default, so the JWT identifies the fork, not the parent, and the parent's trust policy will not accept it.)

The robust fix is a cloud-side condition on the ref_protected / ref / pipeline_source claims — which a YAML linter cannot see. The pipeline-side guard is the part glsec can check, and its absence is a reliable signal that nobody scoped which refs may assume the role.

What glsec checks

GL081 flags a job that declares id_tokens: when either:

  1. Its guard explicitly admits merge_request_event — a rules:if matching merge_request_event (not a when: never exclusion), or a legacy only: merge_requests.
  2. It has no effective guard — no rules: / only: / except:, or a rules: block whose items carry no if: / changes: / exists: condition — and no top-level workflow:rules restricts the pipeline to protected refs.

GL081 does not fire when:

Trigger example

aws-integration-test:
  stage: test
  id_tokens:
    AWS_ID_TOKEN:
      aud: https://sts.amazonaws.com    # correct, scoped audience — but…
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'   # …any MR can mint it
  script:
    - aws sts assume-role-with-web-identity --web-identity-token "$AWS_ID_TOKEN"

Safe alternative

Restrict the job to protected refs, and condition the cloud trust policy on the ref_protected claim so a feature-branch token cannot assume the role:

aws-deploy:
  stage: deploy
  id_tokens:
    AWS_ID_TOKEN:
      aud: https://sts.amazonaws.com
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
  script:
    - aws sts assume-role-with-web-identity --web-identity-token "$AWS_ID_TOKEN"
// AWS IAM trust policy condition (cloud-side, not visible to glsec)
"Condition": {
  "StringEquals": { "gitlab.example.com:sub": "project_path:my/proj:ref_type:branch:ref:main" }
}

Relationship to other rules

False-positive note

Small blast radius by construction — the rule only looks at jobs that already declare id_tokens:, which is opt-in and uncommon. The main false-positive class is a guard expressed somewhere glsec does not resolve (extends:, an included template), which the extends: bail-out already handles. Review each finding against your cloud trust policy: a job restricted only by an unprotected branch condition is still exploitable even when glsec accepts it.

Documentation for glsec 1.23.0. Source of this page · Rule implementation