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

    • For lint/test on PRs: Use 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.
    • Integrate your check:shai-hulud CLI 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
      
  2. 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 to GITHUB_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.
  3. Supply Chain Enhancements:

    • Your CLI is great—automate it in pushes/PRs. Add pnpm if not using (for minimumReleaseAge=3d in .npmrc).
    • Pin all deps (e.g., npm i --save-exact). Run npm audit --audit-level=high weekly via scheduled workflow.
    • Scan for persistence: Extend your CLI to check client node_modules too.
  4. 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_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!

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.