GitLab CI Variable Leakage: Logs, Forks, and Protected Mistakes
GitLab CI variables feel like a vault UI. They behave like environment variables with opinions—protected, masked, environment-scoped, sometimes expanded into YAML in surprising ways. Leakage usually comes from job logs, insecure export debugging, or protected flags that do not match how merges actually run.
Map where variables expand
Read .gitlab-ci.yml for echo, env, set -x, curl debug flags, and scripts that print context. Masked variables hide exact matches in logs; they do not hide a secret that was base64-encoded, split, or written to an artifact.
Tricks that bypass masking:
- Printing character-by-character or as JSON with spaces inserted
- Writing the value to a file then publishing it as an artifact
- Sending the value to an external canary URL from a job you control
- Expanding variables into child pipeline YAML that later logs them
Only do that with a canary variable you created or with explicit permission. Do not exfiltrate live production tokens to your server. A canary named BUGFLARE_CANARY that appears in a public job log is the polite proof.
Protected and fork rules
Protected variables should only inject on protected branches or tags. If a pipeline on a feature branch still receives PROD_API_KEY, the protection story failed. Fork pipelines introduce another boundary: secrets must not run in jobs triggered by untrusted forks unless the project intentionally uses a safe subset.
Check:
- Whether merge request pipelines from forks get high-value variables
- Whether
CI_JOB_TOKENpermissions allow reading other projects' resources - Whether dotenv artifacts pass secrets to downstream deploy jobs that are less trusted
- Whether
environment: productionjobs are actually protected in project settings
I've seen deploy jobs trust a dotenv file produced by a job that contributors can influence. That is variable leakage by composition. Same class of bug as trusting an unreviewed artifact in GitHub Actions.
Also test scheduled pipelines and web triggers. A protected variable that never appears on branch pipelines may still inject into a nightly job whose logs are public. Rules blocks that look careful in YAML sometimes fail open when an if expression is wrong.
GraphQL and API recon
Authenticated as a low-privilege member, project APIs sometimes reveal variable keys (not always values) and environment scopes. Key names alone can show AWS_SECRET_ACCESS_KEY exists on * environment. Combine that with a log leak for impact.
Public projects may expose CI job logs to anonymous users. Search logs for AKIA, PEM headers, and webhook URLs. Group-level variables inherited by every project widen the blast radius when one noisy job exists anywhere in the group.
Report cleanly
Describe the pipeline job, the branch or fork condition, how the value escaped masking, and a redacted snippet. Recommend protected+masked variables, environment scopes, forbidding fork pipelines from protected secrets, artifact allowlists, short-lived OIDC cloud auth instead of static keys, and rotating anything that hit a log.
GitLab's variable UI is only as strong as the job that prints less. Aim your tests at the job, not at the padlock icon.