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:

Recommended Mitigations (directly from the post-mortem):

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:

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:

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):

  1. Add Safe PR Workflows (If Needed):

  2. Audit & Minimize Permissions/Secrets:

  3. Supply Chain Enhancements:

  4. General Hardening:

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:

  1. 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?
  2. What's the difference between on:pull_request and on:pull_request_target (I do use on: 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:

Recommendations to Mitigate:

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.