Skip to content

Commit 93b45bb

Browse files
bschwedlerianpittwood
authored andcommitted
Add supply chain analysis from bitwarden CLI attack
Audit of our Jinja2 image template macros against the attack pattern from the @bitwarden/cli compromise (socket.dev, 2026-04-15). Maps each mutable external reference in our macros to a risk tier and proposes concrete fixes, starting with wait-for-it.j2 (commit pin + SHA256 checksum) as the most acute gap.
1 parent 6308d0c commit 93b45bb

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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

Comments
 (0)