Defending against dependency supply chain attacks
Scenario overview
Modern software development relies heavily on dependencies - third-party packages, libraries, and modules that accelerate development but also introduce security risks. Supply chain attacks targeting dependencies have become increasingly sophisticated, with malicious actors using techniques like typosquatting, package hijacking, account takeovers, and injecting malicious code into legitimate packages.
One example is Shai-Hulud, a self-replicating worm that infiltrated the npm ecosystem via compromised maintainer accounts, injecting malicious post-install scripts into popular JavaScript packages. The malware also stole credentials, injected backdoors, and attempted to destroy the user’s home directory.
Managing dependency threats requires a defense-in-depth strategy. No single control is perfect, so layering multiple defenses creates a more robust security posture. This article provides opinionated guidance on implementing practical security measures that have been field-tested against real-world attacks like Shai-Hulud.
Key design strategies
Layered defenses reduce single points of failure by using multiple controls that reinforce each other - each covers gaps the others leave, so together they form a complete defense. Implement these core strategies:
- Disable package lifecycle scripts by default: Package managers execute scripts automatically during installation (
preinstall,postinstall, etc.). These scripts can run arbitrary code on your system before you’ve reviewed the package contents. Disabling them by default prevents the most common attack vector. - Use dev containers for isolation: Development containers provide strong isolation between host machine and project environment. Even if malicious code executes, it can’t access the actual home directory, SSH keys, or cloud credentials. GitHub Codespaces and Microsoft Dev Box provide managed environments with this isolation built in.
- Require signed commits with user interaction: Cryptographic commit signing with biometric or password authentication prevents malicious scripts from creating commits without your knowledge. This blocks multi-stage attacks that modify your codebase.
- Enforce repository rulesets: Require pull requests and status checks for all changes to protected branches. This creates a checkpoint where automated security scans catch malicious code before it reaches your main branch.
- Establish trusted publishing and verification: Implement OIDC-based trusted publishing to eliminate long-lived tokens, and validate package attestations when consuming dependencies to verify they come from trusted sources.
- Monitor and respond continuously: Automate vulnerability detection with Dependabot, dependency review, code scanning, and secret scanning. Tune alerts to reduce fatigue and establish response runbooks.
Implementation checklist:
- Configure package managers to disable lifecycle scripts by default
- Use dev containers for development, especially for untrusted code
- Enable commit signing with user interaction (passphrase, biometric, or hardware key)
- Configure repository rulesets requiring pull requests and status checks
- Enable Dependabot alerts and security updates for all repositories
- Configure dependency review action as a required status check on pull requests
- Enable code scanning with CodeQL or third-party tools, blocking high-severity issues
- Enable secret scanning with push protection active
- Use trusted publishing with OIDC for any packages you maintain
- Publish packages with provenance attestations (e.g., npm publish –provenance)
- Verify package attestations for dependencies using npm audit signatures in CI/CD
- Configure Dependabot auto-triage rules to reduce alert fatigue
- Create incident response runbooks for different alert severity levels
- Document security exceptions and keep lockfiles current
Assumptions and preconditions
This guidance assumes:
- Package ecosystem: You’re using npm or Yarn for JavaScript/TypeScript projects. Similar principles apply to other ecosystems (pip, Maven, NuGet), but specific configurations differ.
- GitHub repository: You have administrative access to configure repository settings, rulesets, and GitHub Actions workflows.
- GitHub Advanced Security: For organizations using GitHub Enterprise Cloud, GitHub Advanced Security features (Dependabot, dependency review, code scanning) are available. Some features are also available on public repositories.
- Development environment: You can configure your local development environment or are using GitHub Codespaces/dev containers.
- CI/CD with GitHub Actions: Your build and deployment pipelines use GitHub Actions. Adapt the workflow examples if using other CI/CD systems.
Recommended deployment
Layer 1: Disable package lifecycle scripts
The most effective defense against supply chain attacks is to disable automatic execution of package lifecycle scripts. Since these attacks exploit lifecycle scripts to gain initial access, this configuration blocks them completely.
For npm
Create or update your .npmrc file in your home directory or project root:
ignore-scripts=true
save-exact=trueThe ignore-scripts=true setting tells npm to skip all lifecycle scripts during installation. The save-exact=true setting ensures npm saves exact versions (not version ranges) in your package.json, preventing unexpected updates to newer versions that might contain malicious code.
The .npmrc approach is recommended because it applies automatically, so you can’t forget to add the flag. For one-off commands or environments where you can’t modify the config, pass --ignore-scripts directly:
npm ci --ignore-scripts
npm install --ignore-scriptsWhen you need to run scripts for a specific package:
npm rebuild <package-name>For Yarn (v2+)
Create or update your .yarnrc.yml file:
enableScripts: false
enableImmutableInstalls: true
defaultSemverRangePrefix: ""The enableImmutableInstalls: true setting prevents Yarn from modifying your lockfile during install, catching unexpected dependency changes that might indicate tampering.
When a legitimate package needs lifecycle scripts (like building native modules), opt in explicitly in your package.json:
{
"dependenciesMeta": {
"esbuild": {
"built": true
}
}
}.npmrc and .yarnrc.yml configurations to a dotfiles repository to ensure secure defaults follow you across local machines, cloud environments, and GitHub Codespaces.For GitHub Actions workflows
Configure your workflows to install dependencies with lifecycle scripts disabled:
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Build application
run: npm run build.npmrc and .yarnrc.yml files. This enforces script disabling at the runner level, providing defense-in-depth even if workflow configuration is bypassed.When to allow lifecycle scripts
Some legitimate packages require lifecycle scripts (such as node-gyp for native extensions). When you encounter these:
- Review the package source code to understand what the scripts do
- Verify the package publisher is trustworthy
- Document the exception in your security policy
- Enable scripts selectively using
npm rebuild <package-name>after installation - Monitor for changes in subsequent versions
Layer 2: Use dev containers for isolation
Even with package scripts disabled, dev containers provide an additional security boundary. You can run dev containers locally with Docker, or use managed environments like GitHub Codespaces or Microsoft Dev Box that provide container isolation without local setup.
By isolating development in a container, malicious scripts can only access the container’s ephemeral home directory, not your actual files, credentials, or SSH keys. This is exactly the kind of damage the Shai-Hulud attack attempted, destroying home directories when secrets couldn’t be found.
Dev container security benefits
- Limited host access: The container only accesses directories you explicitly mount
- Port visibility: Any attempts to open network ports surface as notifications
- Credential scoping: In Codespaces, Git credentials are automatically scoped to defined repositories
- Ephemeral environment: Destroying the container home directory has no lasting impact
Configure non-root users in VS Code
Ensure your dev container runs as a non-root user:
{
"image": "mcr.microsoft.com/devcontainers/base:noble",
"containerUser": "vscode"
}For additional hardening, consider removing sudo privileges from the container user.
Layer 3: Require signed commits
One subtle risk in supply chain attacks is that malicious code might commit changes to your repository as part of a multi-stage attack. By requiring signed commits with user interaction, you block attempts to create commits without your knowledge.
Configure commit signing
Configure commit signing using a GPG, SSH, or S/MIME key. For maximum protection against automated attacks, use a signing method that requires user interaction. Examples include using a passphrase-protected key, biometric authentication, or a hardware security key.
Commit signing can be enforced via repository rulesets. See Layer 4: Enforce repository rulesets for configuration details.
GitHub provides additional documentation on configuring commit signing.
Layer 4: Enforce repository rulesets
This defense layer happens at the repository level. Rulesets ensure that no code reaches your main branch without going through review and automated security checks.
Configure rulesets
Create a ruleset for your main branch:
- Navigate to Settings → Rules → Rulesets
- Add a new ruleset targeting your default branch
- Examples of protections to configure:
- Require pull requests before merging
- Require signed commits
- Require status checks to pass (including security scans)
- Require at least one approver (preferably two for critical repositories)
- Require re-review when new commits are pushed
- Require review from someone other than the last pusher
- Require code scanning results
- Do not allow bypassing of pull request requirements
Layer 5: Establish trusted publishing and verification
Build trust across the supply chain by establishing cryptographic provenance for packages. This layer works both directions: publishers create trustworthy artifacts, and consumers verify them.
For package publishers: Configure trusted publishing
Trusted publishing removes the need to manage long-lived API tokens in your build systems. Instead, your CI/CD pipeline uses OIDC to authenticate directly with package registries. This eliminates the risk of stolen tokens being used to publish malicious versions.
For packages you maintain:
- Link your GitHub repository as a trusted publisher in your package registry settings (npm, PyPI, RubyGems, etc.)
- Update your release workflow to use OIDC authentication instead of long-lived tokens
- Publish with provenance attestations (e.g.,
npm publish --provenance) - This creates cryptographic proof that the package came from your specific repository at a specific commit
For package consumers: Validate package attestations
Package attestations provide cryptographic proof of a package’s provenance, verifying it was built from specific source code through a verified build process. When consuming packages, validate that they come from trusted publishers.
For packages you depend on:
- Use
npm audit signaturesto verify packages were built through GitHub Actions and identify the source repository and commit - Integrate attestation validation into your CI/CD pipeline for continuous verification
- Prioritize dependencies published with attestations in your dependency selection decisions
Layer 6: Monitor and respond continuously
Automate vulnerability detection and establish processes for responding to security alerts. The goal is visibility into your dependency risk without overwhelming your team with noise.
Configure automated scanning
Build a comprehensive automated detection system that catches vulnerabilities at multiple stages:
Dependency vulnerabilities:
Enable Dependabot to automatically detect vulnerabilities and create pull requests for updates. Consider grouping patch updates for expedited review, assigning security team reviewers, and scheduling daily scans. Use auto-triage rules to reduce alert fatigue by automatically dismissing low-risk alerts or alerts for dependencies that don’t affect your usage. For comprehensive guidance on managing security alerts at scale, see Prioritizing security alert remediation.
Add the dependency review action to your pull request workflows and require it as a status check. Configure it to fail on high-severity vulnerabilities, block problematic licenses, and warn on low OpenSSF Scorecard scores.
Code vulnerabilities and secrets:
Enable code scanning to detect vulnerabilities and coding errors in your source code. Configure CodeQL or third-party tools to run on pull requests and block merges when issues are found.
Enable secret scanning to detect accidentally committed credentials. Configure push protection to prevent secrets from being pushed in the first place, and establish a response runbook for when alerts are triggered.
Review dependency updates systematically
Beyond automated tooling, establish a human review process for evaluating dependency changes:
- Review changelogs and release notes: Before approving any dependency update, check the changelog for breaking changes, security fixes, and new features. Understand what you’re bringing into your codebase.
- Verify maintainer identity: Check the maintainer’s identity and history. Look for recent ownership transfers, new maintainers, or changes in publishing patterns—all potential indicators of account compromise.
- Inspect the diff: Review the actual code changes between versions, especially for dependencies with broad access to your system.
- Don’t trust semver for security decisions: Semantic versioning indicates intended API compatibility, not security risk. A “patch” release can contain arbitrary code changes. Attackers specifically target patch releases because organizations often fast-track them with less scrutiny.
- Group updates for efficient review: Instead of reviewing updates one-by-one, batch dependency updates into scheduled review sessions. This allows focused attention without creating bottlenecks.
- Document decisions: Record why specific versions were approved or rejected. This creates an audit trail and helps future reviewers.
Trade-offs and considerations
Balancing security with development velocity
The most secure approach (reviewing every dependency change manually and disabling all automation) is impractical for most organizations. The key is finding the right balance:
For high-security environments:
- Disable lifecycle scripts with no exceptions except for explicitly approved packages
- Require manual review of all dependency updates
- Validate package attestations and block on missing attestations
- Use allow-lists of approved dependencies
- Mirror packages through private registries (GitHub Packages, Artifactory, Nexus) for additional approval controls
For balanced security:
- Disable lifecycle scripts by default with documented exceptions
- Require human review for all dependency updates (batch for efficiency, not reduced scrutiny)
- Validate attestations when available but don’t block on absence
- Use dependency review action and code scanning as automated safety nets
Common challenges
- Packages requiring lifecycle scripts: Some packages (like
node-gypfor native extensions) legitimately need scripts. Create a documented exception list and usenpm rebuild <package>selectively after installation. - Alert fatigue: Dependabot can generate many alerts. Use auto-triage rules to dismiss low-risk alerts and prioritize what matters.
- Transitive dependencies: You don’t control dependencies of your dependencies. Use
npm audit, Dependabot, dependency review, and code scanning to gain visibility. Consider replacing direct dependencies that bring in vulnerable transitives. - Attestations not universally available: Not all packages support attestations yet. Use attestation availability as one factor in dependency selection and gradually work toward full coverage.
- Keeping lockfiles current: Lockfiles prevent unexpected updates but can become stale. Regularly update dependencies through Dependabot or scheduled audits to ensure security patches aren’t missed while maintaining reproducible builds.
- Breaking changes in security updates: Security updates sometimes include breaking changes that require code modifications. Establish separate processes for security updates (expedited) vs. feature updates (standard review), and allocate time for security debt remediation.
- Workflow security risks: The
pull_request_targettrigger runs with elevated permissions and access to secrets, even for pull requests from forks. Prefer the regularpull_requesttrigger, define least-privilege workflow permissions, and enable CodeQL workflow analysis to detect vulnerabilities.
Seeking further assistance
GitHub Support
Visit the GitHub Support Portal for a comprehensive collection of articles, tutorials, and guides on using GitHub features and services.
Can’t find what you’re looking for? You can contact GitHub Support by opening a ticket.
GitHub Expert Services
GitHub’s Expert Services Team is here to help you architect, implement, and optimize a solution that meets your unique needs. Contact us to learn more about how we can help you.
GitHub Partners
GitHub partners with the world’s leading technology and service providers to help our customers achieve their end-to-end business objectives. Find a GitHub Partner that can help you with your specific needs here.
GitHub Community
Join the GitHub Community Forum to ask questions, share knowledge, and connect with other GitHub users. It’s a great place to get advice and solutions from experienced users.
Related links
GitHub Documentation
For more details about GitHub’s features and services, check out GitHub Documentation.
Specifically, you may find the following links helpful:
- About Dependabot security updates
- About dependency review
- Signing commits
- About rulesets
- npm trusted publishers
- Verifying npm package provenance
- About supply chain security
- Our plan for a more secure npm supply chain - GitHub’s response to the Shai-Hulud attack
- The second half of software supply chain security on GitHub - Build provenance and artifact attestations
- Securing the open source supply chain: The essential role of CVEs - Understanding vulnerability data and automation
External resources
- How I Avoided Shai-Hulud’s Second Coming (Part 1) - Detailed walkthrough of disabling lifecycle scripts and using dev containers
- How I Avoided Shai-Hulud’s Second Coming (Part 2) - Signed commits and repository configuration
- SLSA Framework - Supply-chain Levels for Software Artifacts
- OpenSSF Scorecard - Security health metrics for open source