I'm the owner of ngx-ramblers and I made this commit the other day to hopefully protect my codebase against the shai-hulud exploit on 25 Nov. However I am now looking at the Post-mortem of Shai-Hulud attack on November 24th, 2025, and are seeing that there are a few ways that your CI pipeline can be vulnerable to malicious operators opening PRs on your project and running workflows that can harvest secrets on your main pipeline. Can you review the post mortem and then review my project (note there is a client and server portion) and tell me whether there's anything I should do to tighten it up?
The Shai-Hulud attack (November 24, 2025) was a sophisticated supply chain compromise targeting npm packages like posthog-node (versions 4.18.1, 5.13.3, 5.11.3). It used malicious preinstall scripts to scan for secrets (via tools like Trufflehog), exfiltrate them by creating public GitHub repos, and self-propagate by publishing more malicious packages using stolen npm credentials. The core exploit leveraged GitHub Actions in CI pipelines, specifically:
assign-reviewers.js) via pull_request_target events. This event runs workflows in the context of the base repo (trusted runner, access to secrets) but allows checking out the PR head commit (attacker-controlled code). This executed malicious payloads, stealing secrets like a broad-scope GitHub Personal Access Token (PAT) with repo write access, which was then used to alter workflows and exfiltrate more secrets (e.g., npm tokens).pull_request_target workflows checking out untrusted PR code without isolation.truffleSecrets.json).pull_request_target unless necessary; if used, never checkout PR head (use base repo code only) or run on untrusted runners. For PR testing, prefer pull_request (untrusted context, no secrets access).permissions: { contents: read, actions: read }). Use the ephemeral GITHUB_TOKEN (default, scoped to workflow) over long-lived PATs. Avoid write scopes unless essential.minimumReleaseAge (e.g., 3 days) to block fresh malicious packages. Clean caches aggressively. Adopt trusted publisher models for releases.cloud.json, .github/workflows/discussion.yaml). Run audits (npm/pnpm) and secret scanners regularly.Your repo (https://github.com/nbarrett/ngx-ramblers) is a public, open-source Angular (client) + Express/MongoDB (server) framework for Ramblers Groups sites, with ~266 commits as of late October 2025. It uses AWS S3 for media, Markdown rendering, and various ngx libs for UI/features. The client/server split is clear, with deploy logic (including your new Shai-Hulud check) in server/deploy/.
This adds a proactive check:shai-hulud CLI (in server/deploy/check-shai-hulud.ts) that scans lockfiles (package-lock.json, pnpm-lock.yaml) for:
npm audit/pnpm audit, skippable).truffleSecrets.json, rogue workflow YAMLs).It exits non-zero on hits, with remediation tips, and includes a README-check-shai-hulud.md for usage (local/remote via npx). This is excellent for supply chain defense—aligns perfectly with post-mortem recs on auditing dependencies and detecting persistence. However, it doesn't touch CI workflows (no YAML changes), so it likely runs manually or in a deploy step, not automatically on PRs/push.
Based on your GitHub Actions runs page (as of November 29, 2025), you have three active workflows—all running successfully recently, with no failures or anomalies noted:
| Workflow Name | Triggers | Recent Status | Potential Risks Noted |
|---|---|---|---|
| Deploy and Build Docker Image and Deploy to Fly.io | push to main, pre-main, deploy-fixes |
20+ green runs (e.g., #378–#367, all ~1-2 min) | Deploys to Fly.io likely use secrets (e.g., FLY_API_TOKEN). Safe from PR exploits (no PR trigger). |
| CodeQL | Scheduled (daily/weekly on main) |
6+ green runs (e.g., #66–#61) | GitHub's default security scanner. No custom secrets; analyzes code for vulns. Fully safe. |
| Deploy to Selected Environments | Manual (workflow_dispatch) | 7+ green runs (e.g., #138–#132) | Targeted deploys (e.g., staging/prod). Secrets probable but user-initiated—low risk. |
Key Observations:
pull_request or pull_request_target. This eliminates the primary Shai-Hulud vector (malicious fork PRs running untrusted code to harvest secrets). Your pipeline only activates on trusted pushes, schedules, or manual runs—excellent baseline security.Overall Security Posture: Strong against Shai-Hulud's CI angle. Your commit bolsters supply chain checks, and absence of PR workflows avoids the exploit entirely. No immediate "harvest secrets via malicious PR" risk.
Your setup is already robust, but to align fully with the post-mortem and future-proof (e.g., if you add PR testing for contributors):
Add Safe PR Workflows (If Needed):
on: pull_request (not _target) with permissions: { contents: read }. Checkout with fetch-depth: 0 but no secrets access—use a separate untrusted job for PR head.check:shai-hulud CLI as a step (e.g., npm run check:shai-hulud -- --skip-audit=false). Run audits on every PR/push.ci.yml):name: CI
on: [pull_request]
permissions: { contents: read }
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci # Or pnpm install
- run: npm run check:shai-hulud # Your script
- run: npm test # Client/server tests
Audit & Minimize Permissions/Secrets:
permissions: { contents: write, id-token: write } (only if needed for OIDC/auth). Default to GITHUB_TOKEN—avoid custom PATs.Supply Chain Enhancements:
minimumReleaseAge=3d in .npmrc).npm i --save-exact). Run npm audit --audit-level=high weekly via scheduled workflow.node_modules too.General Hardening:
main.pull_request_target later (e.g., for auto-labeling): Isolate it—no checkouts, no scripts from PR.Nothing urgent—your project dodged the bullet by design. If you share YAML snippets or add PR workflows, I can review deeper. Let me know how else to help!
on:pull_request and on:pull_request_target (I do use on: push: branches: [ "main", "pre-main" ] pull_request: branches: [ "main" ])No, your own postbuild (in client package.json) and postinstall (in server package.json) aren't inherently a problem—they're standard npm lifecycle hooks for running custom logic after building/installing (e.g., asset optimization or env setup). The Shai-Hulud exploit targeted dependencies' malicious preinstall scripts, which run automatically during npm install and could scan/exfiltrate secrets if a compromised package is pulled in.
Potential Risks:
Recommendations to Mitigate:
check:shai-hulud CLI to flag any lifecycle scripts in deps (e.g., via npm ls --json | jq to parse and grep for "scripts" in package metadata). Run npm audit (or pnpm equivalent) in CI to catch vulns that include script exploits.package.json, invoke them manually in your CI workflows (e.g., npm run build && npm run postbuild). This gives you control—skip if suspicious.postinstall, consider a custom setup script that you call post-install, avoiding the hook entirely. For client postbuild, integrate into Angular's ng build --prod via a custom builder if needed.package-lock.json is committed and deps are pinned (e.g., npm i --save-exact). Use pnpm with strict-peer-dependencies and minimumReleaseAge=3d in .npmrc to delay fresh (potentially malicious) packages.--ignore-scripts flag during npm ci (then re-run safe scripts manually), but test thoroughly as it breaks legit deps.Your setup is low-risk overall, especially with your new checker— just automate audits to stay ahead.
on: pull_request and on: pull_request_targetGreat that you're using on: pull_request (the safe default)—that's exactly what avoids the Shai-Hulud CI pitfalls. Here's the breakdown:
| Event | Triggers On | Execution Context | Secret Access | Checkout Behavior | Risk Level |
|---|---|---|---|---|---|
pull_request |
PR opened, synchronized, reopened, or closed (to your repo branches like main). |
Runs on a fresh runner in the PR's fork context (untrusted). Limited to read-only access to the base repo. | No access to repo secrets (e.g., deploy tokens)—they're masked. | Can safely checkout the PR head (attacker code) for testing/linting, but without privileges to abuse it. | Low: Ideal for CI tests. Can't harvest secrets or write back. |
pull_request_target |
Same triggers as above, but only for PRs targeting your branches. | Runs on a trusted runner in the base repo's context (your code/secrets). | Full access to all repo secrets and GITHUB_TOKEN with write perms. |
Allows checkout of PR head, but this mixes trusted env with untrusted code—huge risk if the PR modifies workflows/scripts. | High: Shai-Hulud exploited this to run malicious code from PRs, steal PATs, and escalate. Avoid unless essential (e.g., auto-merging). |
Your Config: With pull_request: branches: [ "main" ], you're golden—PRs from forks (e.g., malicious contributors) trigger safe, secret-free runs for validation. If you ever need secret-dependent PR features (rare), stick to pull_request and use OIDC for auth, or add a separate manual-approval job.