Rules / GL025
GL025 curl/wget with user-controlled CI variable
Severity: warn
OWASP: CICD-SEC-4 – Poisoned Pipeline Execution
What glsec checks
glsec flags curl and wget invocations in job scripts that include a CI variable whose value can be influenced by an external contributor. Whoever controls the variable controls part of the request: extra query parameters or body fields, a different path, and where the variable sits in the host or is passed unquoted, a different host or extra curl options. That can send secrets from the job environment elsewhere or turn the job into a server-side request forgery (SSRF) tool.
Analogous to checkov's CKV_GITLABCI_1.
User-controlled variables
The following variables are fully or partially controlled by the author of a branch, merge request, or commit:
| Variable | Controlled via |
|---|---|
$CI_COMMIT_REF_NAME |
Branch or tag name |
$CI_COMMIT_REF_SLUG |
Branch or tag name (slugified) |
$CI_COMMIT_BRANCH |
Branch name |
$CI_COMMIT_TAG |
Tag name |
$CI_COMMIT_MESSAGE |
Commit message |
$CI_COMMIT_TITLE |
First line of commit message |
$CI_COMMIT_DESCRIPTION |
Body of commit message |
$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME |
MR source branch |
$CI_MERGE_REQUEST_TITLE |
MR title |
Trigger example
notify:
script:
- curl https://hooks.example.com/notify?branch=$CI_COMMIT_REF_NAME
- curl -d "msg=$CI_MERGE_REQUEST_TITLE" https://api.example.com/comment
If an attacker names their branch hooks.attacker.com/steal?token= and the job has secrets in its environment, the request can be redirected.
Safe alternatives
Sanitize the variable before use:
notify:
script:
- |
BRANCH=$(printf '%s' "$CI_COMMIT_REF_NAME" | tr -cd '[:alnum:]-_.')
curl "https://hooks.example.com/notify?branch=${BRANCH}"
Use a fixed URL and pass the value only in a controlled body field:
notify:
script:
- curl -X POST https://hooks.example.com/notify \
--data-urlencode "branch=$CI_COMMIT_REF_NAME"
--data-urlencode percent-encodes the value, preventing URL injection.
Restrict pipelines to protected branches so untrusted commits never run with secrets.
Notes
- One finding is emitted per offending script entry (unlike most GL rules which emit one per job), because each entry is an independent request that may reach a different host.
- Each shell line of a multi-line entry is checked on its own, with
\continuations joined. A variable assigned from a user-controlled one on an earlier line counts as that variable, unless the assignment sanitises it withtr -cd/tr -dc,sedor jq's@uri. - A value passed with
--data-urlencodeis not flagged: curl percent-encodes it, so it can't change the host, the path or other fields. - The rule checks
before_script,script,after_script, andrun:step scripts. - Variables that are not in the list above (e.g.
$MY_TOKEN,$CI_PROJECT_ID) do not trigger this rule. - If you have a genuine need and the value is sanitised inline, suppress with:
# glsec:ignore GL025 -- value sanitised above.
Documentation for glsec 1.23.0. Source of this page · Rule implementation