Protecting project against the Shai-Hulud Exploit

Question Originally Asked in local codex session

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?

Security Review Summary (NGX‑Ramblers)

1. Third‑Party server Package in Frontend Build (High)

Your frontend package.json brings in the public npm package server@1.0.41 (package.json:84, package-lock.json:34690). This is unrelated to your checked‑in Express backend under /server, so every npm ci executes that third‑party package (and its install scripts), recreating the same supply‑chain attack surface used by Shai‑Hulud.

{
  ...
  "dependencies": {
    ...
    "rxjs": "7.8.2",
    "server": "1.0.41",
    "turndown": "7.2.0"
  }
}

Action: Remove it (or convert to "server": "file:server" if you meant to re-use the local code) so CI never runs unvetted install hooks.

2. Deploy Workflow Runs on Pull Requests with Secrets (High)

.github/workflows/build-push-and-deploy-ngx-ramblers-docker-image.yml triggers on both push and pull_request (lines 3‑7). The deploy job later loads CONFIGS_JSON, Docker, and Fly secrets (lines 65‑109). An attacker with any contributor access could open a PR and immediately exfiltrate secrets—exactly the PostHog scenario.

  on:
    push:
      branches: [ "main", "pre-main" ]
    pull_request:
      branches: [ "main" ]
  ...
    deploy:
      needs: test
      runs-on: ubuntu-latest
      steps:
        ...
        - name: Create configs.json from secrets
          env:
            CONFIGS_JSON: ${{ secrets.CONFIGS_JSON }}
          run: |
            echo "$CONFIGS_JSON" > non-vcs/fly-io/configs.json

Action: Restrict deployments to push events (or wrap the deploy job with if: github.event_name == 'push') so PRs never receive secrets.

3. Default GITHUB_TOKEN Permissions (Medium)

Neither workflow sets permissions, so GitHub grants the default token repo write access. If any dependency becomes compromised (e.g., via npm ci), the attacker already holds a write-capable token.

Action: Add explicit permissions blocks per job:

permissions: contents: read packages: write # only where needed

4. Floating Action References and Global Installs (Medium)

Both workflows use floating references (superfly/flyctl-actions/setup-flyctl@master) and npm install -g ts-node, which pull arbitrary code at runtime.

  - name: Install ts-node
    run: npm install -g ts-node

  - name: Install Flyctl
    uses: superfly/flyctl-actions/setup-flyctl@master

Action: Pin actions by commit SHA and leverage the repo’s checked-in ts-node via npx ts-node … to keep builds within locked dependencies.

5. npm ci Executes Third-Party Scripts in PR Jobs (Medium)

Both workflows run npm ci with default script execution (build-push-and-deploy...:23-44, deploy-to-environments.yml:69-74). Shai-Hulud exploited preinstall hooks.

Action: Use npm ci --ignore-scripts for PR/test jobs and explicitly execute the scripts you control afterward (e.g., npx ng test, npm run build).

6. Shai-Hulud Scanner Misses Published IoCs (Medium)

server/deploy/check-shai-hulud.ts only checks for three persistence artifacts:

const suspicious: string[] = ["cloud.json", "truffleSecrets.json", ".github/workflows/discussion.yaml"];

The PostHog post-mortem listed additional files (setup_bun.js, bun_environment.js, contents.json, environment.json).

Action: Expand the list:

  const suspicious = [
    "cloud.json",
    "truffleSecrets.json",
    ".github/workflows/discussion.yaml",
    "setup_bun.js",
    "bun_environment.js",
    "contents.json",
    "environment.json"
  ];

7. Scanner Not Wired Into CI (Low)

The CLI is defined in server/package.json:

  "check:shai-hulud": "tsx deploy/check-shai-hulud.ts"

…but no workflow runs it. A compromised lockfile could therefore merge unnoticed.

Action: Add a lightweight GitHub Actions job:

  - name: Shai-Hulud scan
    run: npm run --prefix server check:shai-hulud -- --skip-audit

  Trigger it whenever lockfiles or dependency manifests change.

8. Environment Secret Fan-Out (Low)

Workflows hydrate the entire CONFIGS_JSON (all environments) before deploying a single environment. A compromised job gains every environment’s secret set.

Action: Split secrets per environment (via GitHub Environments or separate secrets) so jobs only materialize what they deploy.

Command Output (Reference)

$ npm run --prefix server check:shai-hulud -- --path .. --skip-audit
  node:net:1915
        const error = new UVExceptionWithHostPort(rval, 'listen', address, port);
  ...
  Error: listen EPERM: operation not permitted /var/folders/.../tsx-502/40273.pipe

(The CLI fails locally due to sandbox restrictions but should run normally on GitHub-hosted runners.)

Next Steps

  1. Remove or relink the npm server dependency, re-lock, and re-run the scanner.
  2. Update workflows: restrict deploy job to pushes, set explicit permissions, pin all third-party actions/binaries, and run npm ci --ignore-scripts for PR jobs.
  3. Extend the Shai-Hulud CLI to cover all IoCs and wire it into CI so compromised lockfiles or persistence files block merges.

• Key Findings

Open Questions

Next Steps

  1. Remove/replace the stray server npm dependency and re-lock.
  2. Restrict the deploy workflow to push events, add explicit permissions, and pin third-party actions/binaries.
  3. Update CI install steps to minimize third-party install hooks, and integrate the Shai-Hulud scanner with an expanded IoC list so lockfile compromises are caught automatically.