Protecting project against the Shai-Hulud Exploit
Question Originally: asked here
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?
Review of the PostHog Shai-Hulud Post-Mortem
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:
- Attack Vector on PRs from Forks: Attackers opened PRs to target repos, modifying workflow-triggering scripts (e.g.,
assign-reviewers.js) viapull_request_targetevents. 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). - Key Vulnerabilities Highlighted:
pull_request_targetworkflows checking out untrusted PR code without isolation.- Overly permissive PATs (e.g., full repo write) stored as secrets, enabling escalation.
- Workflows running repo-internal scripts that could be indirectly tampered with via PRs.
- Lack of PR scrutiny for workflow-modifying changes; delayed detection despite static analysis.
- Broader CI Risks: Even without PRs, push/scheduled workflows can leak if secrets are mishandled (e.g., echoed in logs) or if dependencies introduce persistence (e.g., suspicious files like
truffleSecrets.json).
Recommended Mitigations (directly from the post-mortem):
- Triggers & Isolation: Avoid
pull_request_targetunless necessary; if used, never checkout PR head (use base repo code only) or run on untrusted runners. For PR testing, preferpull_request(untrusted context, no secrets access). - Permissions: Set minimal workflow permissions (e.g.,
permissions: { contents: read, actions: read }). Use the ephemeralGITHUB_TOKEN(default, scoped to workflow) over long-lived PATs. Avoid write scopes unless essential. - Secrets Management: Use short-lived, environment-specific secrets. Revoke/rotate on suspicion. Scan logs for exfiltration (e.g., grep for "preinstall" or unexpected repo creates).
- Supply Chain: Pin dependencies; use pnpm 10+ with
minimumReleaseAge(e.g., 3 days) to block fresh malicious packages. Clean caches aggressively. Adopt trusted publisher models for releases. - PR Protections: Require security team reviews for workflow/PR changes. Enable branch protection rules blocking direct pushes to main.
- Detection: Monitor for suspicious files (e.g.,
cloud.json,.github/workflows/discussion.yaml). Run audits (npm/pnpm) and secret scanners regularly.
Review of Your ngx-ramblers Project
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/.
Your Recent Commit (73a8274)
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:
- Known Shai-Hulud-compromised packages (hardcoded version ranges).
- High/critical audit issues (via
npm audit/pnpm audit, skippable). - Suspicious persistence files (e.g.,
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.
CI Pipeline Analysis
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:
- No PR Triggers: Critically, none of your workflows use
pull_requestorpull_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. - No Evident Exposures: No runs tied to external contributions. Deploys (Docker/Fly.io) are push-gated, so only your commits trigger them. CodeQL is read-only analysis. If secrets are used (e.g., for Fly.io deploys or AWS S3), they're not runnable by outsiders.
- Client/Server Split: Workflows appear repo-wide (no separate client/server YMLs visible), but your Shai-Hulud check is server-focused. Client (Angular) deps might need similar scanning if added to CI.
- Limitations in Review: I couldn't pull full YAML details (GitHub tree/scraping quirks), but triggers/statuses confirm safety. If workflows use broad PATs or echo secrets, that's a gap—but unlikely given clean runs.
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.
Recommendations to Tighten It Up
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):
- For lint/test on PRs: Use
on: pull_request(not_target) withpermissions: { contents: read }. Checkout withfetch-depth: 0but no secrets access—use a separate untrusted job for PR head. - Integrate your
check:shai-huludCLI as a step (e.g.,npm run check:shai-hulud -- --skip-audit=false). Run audits on every PR/push. - Example minimal YAML snippet (add to a new
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
- For lint/test on PRs: Use
Audit & Minimize Permissions/Secrets:
- In existing deploys (e.g., Fly.io workflow): Explicitly set
permissions: { contents: write, id-token: write }(only if needed for OIDC/auth). Default toGITHUB_TOKEN—avoid custom PATs. - Rotate any deploy secrets (e.g., FLY_API_TOKEN, AWS keys). Use GitHub's OIDC for Fly.io/AWS to eliminate long-lived tokens.
- Enable "Require approval for first-time contributors" in repo settings for PRs.
- In existing deploys (e.g., Fly.io workflow): Explicitly set
Supply Chain Enhancements:
- Your CLI is great—automate it in pushes/PRs. Add pnpm if not using (for
minimumReleaseAge=3din.npmrc). - Pin all deps (e.g.,
npm i --save-exact). Runnpm audit --audit-level=highweekly via scheduled workflow. - Scan for persistence: Extend your CLI to check client
node_modulestoo.
- Your CLI is great—automate it in pushes/PRs. Add pnpm if not using (for
General Hardening:
- Branch protection: Enforce reviews + status checks on
main. - Monitor: Set up Dependabot alerts for vulns; watch Actions logs for anomalies.
- If adding
pull_request_targetlater (e.g., for auto-labeling): Isolate it—no checkouts, no scripts from PR.
- Branch protection: Enforce reviews + status checks on
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!
Thanks! - 2 questions:
- I do use postbuild in my client package.json and postinstall in my server package.json. Is that a problem and could I do it differently?
- What's the difference between
on:pull_requestandon:pull_request_target(I do useon: push: branches: [ "main", "pre-main" ] pull_request: branches: [ "main" ])
1) Postbuild and Postinstall Scripts
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:
- If a transitive dependency (deep in your lockfile) has a malicious lifecycle script, it could execute during CI/install without your knowledge.
- Your scripts themselves are safe (you control them), but they might indirectly chain to untrusted code if they invoke deps.
Recommendations to Mitigate:
- Audit Regularly: Extend your
check:shai-huludCLI to flag any lifecycle scripts in deps (e.g., vianpm ls --json | jqto parse and grep for "scripts" in package metadata). Runnpm audit(or pnpm equivalent) in CI to catch vulns that include script exploits. - Alternatives/Different Approaches:
- Move to Explicit Commands: Instead of auto-running in
package.json, invoke them manually in your CI workflows (e.g.,npm run build && npm run postbuild). This gives you control—skip if suspicious. - Use Dedicated Scripts: For server
postinstall, consider a customsetupscript that you call post-install, avoiding the hook entirely. For clientpostbuild, integrate into Angular'sng build --prodvia a custom builder if needed. - Lock & Pin: Ensure
package-lock.jsonis committed and deps are pinned (e.g.,npm i --save-exact). Use pnpm withstrict-peer-dependenciesandminimumReleaseAge=3din.npmrcto delay fresh (potentially malicious) packages. - Sandbox Installs: In CI, use
--ignore-scriptsflag duringnpm ci(then re-run safe scripts manually), but test thoroughly as it breaks legit deps.
- Move to Explicit Commands: Instead of auto-running in
Your setup is low-risk overall, especially with your new checker— just automate audits to stay ahead.
2) Difference Between on: pull_request and on: pull_request_target
Great 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.