|
| 1 | +# Bitwarden CLI supply chain attack: applicability to Posit container images |
| 2 | + |
| 3 | +## What happened |
| 4 | + |
| 5 | +On 2026-04-15, attackers compromised `@bitwarden/cli` v2026.4.0 on npm by exploiting a GitHub Action in Bitwarden's CI/CD pipeline. Malicious code (`bw1.js`) was injected during the build process before publication. The payload harvested GitHub tokens, AWS/Azure/GCP credentials, npm tokens, SSH keys, and Claude/MCP settings from CI environments. Stolen npm tokens were reused to republish other packages with malicious preinstall hooks, propagating the attack. |
| 6 | + |
| 7 | +Source: https://socket.dev/blog/bitwarden-cli-compromised |
| 8 | + |
| 9 | +## Attack pattern |
| 10 | + |
| 11 | +1. Compromise a GitHub Action used in the target's CI pipeline |
| 12 | +2. Inject credential-harvesting code during the build step |
| 13 | +3. Published artifact looks legitimate — version bump, normal release workflow |
| 14 | +4. Consumers install the package and execute the payload in their own CI/build environments |
| 15 | +5. Stolen tokens propagate the attack to more packages |
| 16 | + |
| 17 | +## Applicability to our image templates |
| 18 | + |
| 19 | +Our Jinja2 macros download and execute external code during `docker build`. A compromised upstream source would inject malicious code into every image build — the same vector as the bitwarden attack, but at the container layer. |
| 20 | + |
| 21 | +### Mutable references (HIGH — no integrity verification) |
| 22 | + |
| 23 | +| Macro | Line | What it does | Risk | |
| 24 | +|---|---|---|---| |
| 25 | +| `wait-for-it.j2` | 5 | `curl` from `master` branch of `rstudio/wait-for-it`, no checksum | Mutable branch ref. Anyone with write access pushes arbitrary shell code into every image that uses this macro. | |
| 26 | +| `r.j2` | 25 | `curl \| bash` via `rstd.io/r-install` URL shortener | URL shortener adds a redirect layer. If `rstd.io` or the target script is compromised, arbitrary code runs. First-party Posit script, but still unverified. | |
| 27 | +| `apt.j2` | 8 | `curl \| bash` for Cloudsmith repo setup (`dl.posit.co/public/pro/setup.deb.sh`) | First-party, but `curl \| bash` with no checksum. | |
| 28 | +| `dnf.j2` | 13 | `curl \| bash` for Cloudsmith repo setup (`dl.posit.co/public/pro/setup.rpm.sh`) | Same as above, RPM variant. | |
| 29 | + |
| 30 | +### Mutable container tags (MEDIUM — tag can be re-pushed) |
| 31 | + |
| 32 | +| Macro | Line | What it does | Risk | |
| 33 | +|---|---|---|---| |
| 34 | +| `python.j2` | 47 | `FROM ghcr.io/astral-sh/uv:debian-slim` | Mutable tag. A compromised UV image injects into all Python builds. Cannot trivially pin to a digest because the tag intentionally floats to pick up new Python version support. | |
| 35 | + |
| 36 | +### Version-pinned but unverified (LOW — version in URL, no checksum) |
| 37 | + |
| 38 | +| Macro | Line | What it does | Risk | |
| 39 | +|---|---|---|---| |
| 40 | +| `quarto.j2` | 42 | Downloads tarball from GitHub releases by version | URL is version-pinned, so it requires compromising the specific release asset. Quarto publishes SHA256 checksums alongside releases that we could verify. | |
| 41 | + |
| 42 | +## What's already been done |
| 43 | + |
| 44 | +From posit-dev/platform-team#3 (fork-safe CI workflows): |
| 45 | + |
| 46 | +- **PR #441** (merged): SHA256 checksum verification for `setup-goss` and `setup-hadolint` composite actions |
| 47 | +- **PR #443** (merged): SHA pinning for all third-party GitHub Actions + zizmor CI linting |
| 48 | +- **PR #440** (merged): Script injection fixes in shared workflows |
| 49 | +- **Issue #453** (open): Sigstore attestations for published container images |
| 50 | + |
| 51 | +## Recommended changes |
| 52 | + |
| 53 | +### 1. Pin and checksum `wait-for-it.j2` (immediate) |
| 54 | + |
| 55 | +Pin the download URL to commit SHA `81b1373f17855a4dc21156cfe1694c31d7d1792e` and add SHA256 verification: |
| 56 | + |
| 57 | +```jinja2 |
| 58 | +{%- set _COMMIT_SHA = "81b1373f17855a4dc21156cfe1694c31d7d1792e" -%} |
| 59 | +{%- set _SHA256 = "b7a04f38de1e51e7455ecf63151c8c7e405bd2d45a2d4e16f6419db737a125d6" -%} |
| 60 | +
|
| 61 | +{% macro install() -%} |
| 62 | +curl -fsSL -o /usr/local/bin/wait-for-it.sh https://raw.githubusercontent.com/rstudio/wait-for-it/{{ _COMMIT_SHA }}/wait-for-it.sh && \ |
| 63 | +echo "{{ _SHA256 }} /usr/local/bin/wait-for-it.sh" | sha256sum -c - && \ |
| 64 | +chmod +x /usr/local/bin/wait-for-it.sh |
| 65 | +{%- endmacro %} |
| 66 | +``` |
| 67 | + |
| 68 | +### 2. Pin UV base image to versioned tag (short-term) |
| 69 | + |
| 70 | +Replace `ghcr.io/astral-sh/uv:debian-slim` with a version-pinned tag like `ghcr.io/astral-sh/uv:0.7.8-debian-slim`. Add a Dependabot or Renovate entry to keep it updated. This preserves reproducibility while allowing controlled updates. |
| 71 | + |
| 72 | +Pinning to a digest (`@sha256:...`) is more secure but would require manual updates whenever a new Python version needs UV support. |
| 73 | + |
| 74 | +### 3. Add checksum verification to Quarto downloads (short-term) |
| 75 | + |
| 76 | +Quarto publishes checksums at `https://github.com/quarto-dev/quarto-cli/releases/download/v{version}/quarto-{version}-checksums.txt`. Download and verify in the macro: |
| 77 | + |
| 78 | +```jinja2 |
| 79 | +curl -fsSL -o /tmp/quarto-checksums.txt "https://github.com/quarto-dev/quarto-cli/releases/download/v{{ version }}/quarto-{{ version }}-checksums.txt" && \ |
| 80 | +curl -fsSL -o /tmp/quarto.tar.gz "https://...tar.gz" && \ |
| 81 | +grep "quarto-{{ version }}-linux-${TARGETARCH}.tar.gz" /tmp/quarto-checksums.txt | sha256sum -c - && \ |
| 82 | +``` |
| 83 | + |
| 84 | +### 4. Evaluate first-party `curl | bash` scripts (longer-term) |
| 85 | + |
| 86 | +The `rstd.io/r-install` and `dl.posit.co` Cloudsmith setup scripts are first-party Posit resources. The risk is lower, but the pattern is still `curl | bash` without verification. Options: |
| 87 | + |
| 88 | +- Pin the R install script to a tagged release and verify its checksum |
| 89 | +- For Cloudsmith setup scripts: these change when Cloudsmith updates their onboarding, so pinning is harder. Consider vendoring or checksumming. |
| 90 | + |
| 91 | +### 5. Complete Sigstore attestations (tracked) |
| 92 | + |
| 93 | +Already filed as posit-dev/images-shared#453. This provides end-to-end provenance for the published images themselves, complementing the build-time integrity checks above. |
0 commit comments