A Threat Model of the Dependency Update Path: What Dependabot Protects and What It Exposes

Automated dependency updates close a real gap: a project that never updates keeps every published vulnerability. Dependabot does this at scale, and in doing so it creates a path that delivers third-party code into a repository on a schedule, under a name developers have learned to trust.

That path is an attack surface of its own. This article builds a threat model of it from the point of view of a project using Dependabot, and tests the model against public incidents. The earlier articles in this series cover how a job runs and how the credential proxy isolates it, so this one concentrates on what happens around the job.

The model follows the four questions of the OWASP threat modeling process: what are we working on, what can go wrong, what do we do about it, and did we do a good job. It stays at the level of design and defence, and contains no attack instructions. Likelihood and impact ratings are my own judgement.

This article has been made with the help of Claude Code and several custom skills

What you need to know first

The article relies on a few concepts from package ecosystems and GitHub Actions.

  • Install script. Package managers such as npm can run a package’s scripts during installation, before any application code calls it. A malicious version can therefore act when it is installed.
  • Mutable reference. A git tag or a branch can be moved to another commit. A full commit SHA cannot, because it identifies the content.
  • pull_request and pull_request_target. The first runs in the context of the pull request, with limited rights for forks. The second runs in the context of the base repository, with its secrets and write access, so it needs great care.
  • github.actor. The account that triggered the latest workflow event. It is not necessarily the author of the pull request.
  • Confused deputy. A privileged component is tricked into using its authority on behalf of someone who lacks it, classified as CWE-441.
  • Provenance attestation. A signed statement of how and where a package was built.
  • STRIDE. A mnemonic for threat categories: spoofing, tampering, repudiation, information disclosure, denial of service and elevation of privilege.

What are we working on

The system under analysis runs from a package maintainer’s account to code running in production.

flowchart LR
    M["Package maintainer<br/>and registry"] -->|"new release"| D["Dependabot job"]
    ADV["Advisory Database"] -.->|"security updates"| D
    D -->|"pull request"| PR["Pull request"]
    PR -->|"triggers"| CI["CI workflows"]
    CI -->|"status"| REV{"Review or auto-merge"}
    REV -->|"merge"| MAIN["Default branch"]
    MAIN -->|"build and deploy"| PROD["Production"]

Each arrow crosses a trust boundary:

  1. Registry to Dependabot. A release is written by whoever controls the maintainer’s account.
  2. Dependabot to the repository. The job writes files and text into a pull request.
  3. Pull request to CI. New dependency code can run in the project’s pipeline.
  4. CI to the default branch. A person or a workflow decides to merge.

The assets at stake are the repository’s integrity, CI secrets, signing and publishing credentials, developer machines that install the merged lockfile, and production systems.

The attackers in scope are a thief of a maintainer’s account, a maintainer who turns hostile, an outside user who can open a pull request or comment on a public repository, and someone holding a stolen token. GitHub’s own infrastructure is out of scope as a threat.

What can go wrong

The table lists the main threats, with STRIDE categories in the second column. The ratings assume a typical open-source or company repository with Dependabot enabled.

ID Threat STRIDE Likelihood Impact
T1 A short-lived malicious release is proposed Tampering High High
T2 A patient backdoor passes every time-based control Tampering Medium High
T3 A dependency runs code in CI when the pull request opens Elevation of privilege Medium High
T4 A mutable reference changes under a pinned-looking version Tampering Medium High
T5 An auto-merge workflow is confused about who acted Elevation of privilege Medium High
T6 Commits are forged to look like Dependabot’s Spoofing Medium High
T7 A security update ships a malicious release, with no cooldown Tampering Low High
T8 Third-party text in the pull request misleads a reader or a tool Spoofing Medium Medium
T9 Volume wears down review Repudiation High Medium

T1: A short-lived malicious release

The most common pattern is an account takeover followed by a malicious version that lives for hours. GitHub’s case for a cooldown cites compromised npm packages, including chalk and debug, that are downloaded more than 2 billion times a week. Their poisoned versions were live for roughly two hours.

The Shai-Hulud worm of September 2025 showed the next step: a post-install script harvested sensitive data, and npm tokens found in the environment were used to publish malicious versions of other packages. Reported package counts vary by source, from about 180 to over 500.

An update bot that reacts within minutes can deliver such a release before it is removed, which is why the cooldown exists. It is on by default since July 2026: version updates wait three days, and the code confirms this with DEFAULT_COOLDOWN_DAYS = 3.

T2: A patient backdoor

The same blog post states the limit: a cooldown “does little against attacks that play a longer game”, such as backdoors planted in releases and left dormant. A release that stays quiet for longer than the window is proposed like any other. Time-based controls assume someone notices. If nobody does, waiting achieves nothing.

T3: Code running in CI

A dependency pull request runs the project’s tests, and that means installing the new version. So a malicious package can reach the CI runner before anyone reviews the change.

GitHub reduces this. Its documentation says Dependabot-triggered runs are treated like fork pull requests: they get a read-only GITHUB_TOKEN, and the only secrets available are Dependabot secrets. That limit is real, but it depends on what the project stores there. A publishing token added as a Dependabot secret to make a job pass is exposed to the code being tested.

T4: Mutable references

The tj-actions/changed-files compromise of March 2025 (CVE-2025-30066) shows the risk. According to StepSecurity, attackers “retroactively updated multiple version tags to reference the malicious commit”. The code read secrets from runner memory and printed them to build logs, where public repositories exposed them.

No Dependabot pull request was needed: a workflow pinned to @v45 received the change on its next run. This threat is outside what an update tool sees, because the version number did not change. Pinning to full commit SHAs avoids it, and Dependabot’s github_actions ecosystem handles SHA pins, so pinning does not mean giving up updates.

T5: A confused auto-merge

Teams automate Dependabot merges with a workflow that checks the actor. Boost Security’s write-up explains the flaw: github.actor is whoever triggered the latest event, not who wrote the pull request. If an outsider can cause Dependabot to act on a pull request, the check passes although the code came from elsewhere. Combined with pull_request_target, this gives a privileged workflow to untrusted code.

Kong’s post-mortem of the December 2024 Ingress Controller incident describes a Pwn Request “enabled through a Dependabot actor confusion attack”, through which an unauthorised image containing a cryptominer was published. Two details matter here:

  • The pull request targeted an old branch that still had the vulnerable workflow, because an earlier fix had missed unused branches.
  • Dependabot had been allowlisted for privileged workflows, so its name opened the door.

GitHub removed several comment commands in January 2026, including merge, which closed one of the techniques the article describes. Boost Security notes that the other remains valid.

T6: Forged Dependabot commits

Git authorship is a claim, not a proof. Checkmarx reported commits to hundreds of repositories in July 2023, pushed with stolen personal access tokens and made to look like Dependabot’s. It noted why this works: developers often do not review Dependabot changes closely.

Dependabot was not compromised here. Its reputation was the asset being abused. Any process that treats a bot’s name as evidence of safety inherits this weakness.

T7: A malicious security update

Security updates skip the cooldown, because delaying a fix for a public flaw has its own cost. They also choose the lowest non-vulnerable version. This combination opens a narrow path, which is my own inference and has no public incident behind it: if the release that fixes a real advisory is itself malicious, it can be proposed immediately, with urgency attached.

The bar is high, since it needs a real advisory and control of the release at the right moment. It is rated low for that reason, and it is a reason not to treat “security fix” as a synonym for “safe to auto-merge”.

T8: Text from third parties

A Dependabot pull request contains release notes, changelogs and commit messages written by the dependency’s authors. dependabot-core runs that text through sanitize_links_and_mentions, which addresses unwanted notifications and links. It does not judge whether the text is truthful.

A release note can claim a change is harmless, and readers tend to believe it. This applies with more force to automated reviewers: a tool or language model that reads the pull request body is reading attacker-influenced input. That second point is my own reasoning, and I found no public incident that shows it for Dependabot.

T9: Review fatigue

The threats above assume someone looks at the change. Yet Dependabot’s volume teaches the opposite habit, and Checkmarx named it as the reason forged commits went unnoticed. A grouped update adds to this, since one approval then covers many packages.

Fatigue is rated medium in impact because it does not act alone, but it amplifies T1, T2, T6 and T8.

What do we do about it

The controls below map to the threats. Several already exist in Dependabot, and the others are the project’s own responsibility.

Control Addresses Who provides it Limit
Cooldown, 3 days by default T1 Dependabot Fails against T2, and does not apply to T7
Pin actions to full SHAs T4 Project, with Dependabot updating the pins Requires review of each pin change
Identify by pull request author, not actor T5 Project Must apply to every branch
No secrets beyond what tests need T3 Project Needs a review of Dependabot secrets
Commit signature verification T6 Project and GitHub Useful only if something enforces it
Lockfile and install without scripts in CI T1, T3 Project Some packages need scripts
Metadata warnings in the pull request T1, T2 Dependabot Limited ecosystems, needs a reader
Human review of bumps T2, T7, T8, T9 Project Costs time

Cooldown: what to expect

The evidence for a cooldown is good for what it covers. GitHub says a review of 21 incidents from 2018 to 2026 found that a cooldown could have filtered out the majority of short-lived publishes. William Woodruff’s November 2025 analysis found that 8 of 10 attacks had windows of opportunity of less than a week. The two exceptions were xz-utils, at about five weeks, and Kong Ingress Controller, at about ten days, so a three-day wait would not have covered those.

Two caveats apply. The first is that incident lists contain the attacks that were detected, which favours ones that were found quickly. The second is that a cooldown works because somebody else installs the release first, or a scanner sees it. If everyone waits, detection has to come from scanners.

Keep the default unless there is a reason to change it. For sensitive repositories, a longer window on major versions is a reasonable choice.

Auto-merge: a stricter rule

Auto-merge is the point where most of these threats turn into incidents, because it removes the last human check. If it is used:

  • Trigger on pull_request, not pull_request_target, wherever possible.
  • Check who wrote the pull request, as in github.event.pull_request.user.login, not github.actor. Boost Security also warns against other contexts that can be confused, such as github.triggering_actor.
  • Use dependabot/fetch-metadata over custom parsing, and pin it to a SHA.
  • Limit it to patch updates of development dependencies, where a mistake is cheapest.
  • Apply the same workflow fix to every branch, including old ones. Kong’s case shows why.
  • Do not allowlist Dependabot for privileged workflows.
# Weak: the actor is whoever triggered the latest event
if: github.actor == 'dependabot[bot]'

# Better: the author of the pull request
if: github.event.pull_request.user.login == 'dependabot[bot]'

The check on the author is necessary, not sufficient. Boost Security also advises avoiding synchronize events where possible, and protecting dependabot/** branches if Dependabot has bypass privileges.

Signals that already exist

Dependabot now reports some supply-chain signals in the pull request body. The base MetadataFinder has three hooks, and at the analysed commit they are implemented unevenly:

Signal npm and Yarn Python Bun
Maintainer changes Yes Yes Yes
Install script changes Yes No No
Lost provenance attestation Yes No No

The npm attestation check is a good example of a useful signal: it warns when a previous version was attested and the new one is not, which is what a release published from a stolen token often looks like. A new maintainer, a new install script or a lost attestation should stop auto-merge and draw a reader’s attention. These checks do nothing if no one reads the message.

Solidity projects

Smart-contract projects need a separate note, because their dependencies can control funds directly. Dependabot’s public code has no ecosystem for Foundry. Foundry’s lib/ folders are ordinarily git submodules, so the git_submodules ecosystem can propose updates, and npm-based projects use npm_and_yarn.

A submodule bump is a commit range, not a package version, and no cooldown history or attestation applies in the same way. For contract libraries, an update changes code that gets deployed and is hard to undo, so automated merging is the wrong default at any patch level. Also, the code running in CI and on developer machines handles deployment keys, which makes T3 more serious here.

Did we do a good job

The incidents confirm most of the model, and they expose where it is weakest.

Incident Threat Which control would have helped
npm chalk and debug T1 Cooldown
Shai-Hulud, September 2025 T1, T3 Cooldown, scripts disabled in CI, fewer secrets
tj-actions/changed-files, March 2025 T4 SHA pinning
Kong Ingress Controller, December 2024 T5 Author check, no allowlisting, all branches fixed
Forged Dependabot commits, July 2023 T6 Fine-grained tokens, review, signature enforcement

The right-hand column is hindsight, and it is my assessment rather than the finding of any report. Four observations follow.

Dependabot was seldom the compromised component. Across these cases, the job itself was not breached. The failures were in workflows built around it, in tokens, or in trust placed in its name.

Most controls are the project’s responsibility. The defaults have improved, with the cooldown and read-only tokens. But Kong’s incident came from a workflow the project itself had written.

T7 and T8 have no incident behind them. They are reasoned from the design. They could be wrong, or simply not yet seen.

The counts of malicious packages are large. GitHub reports more than 6,500 npm malware advisories in the year ending May 2026. Most of these will not affect a given project, but the rate shows that malicious versions are published every day.

The model should be revisited when GitHub changes Dependabot’s comment commands or defaults, when a project adds auto-merge, and after any incident in a dependency.

Conclusion

An update bot delivers third-party code on a schedule, under a trusted name. Its risks sit mainly around the job.

  • A cooldown addresses short-lived releases, which is the most common pattern, and it fails against patient backdoors.
  • Mutable references bypass update tools. tj-actions changed under an unchanged version number, and SHA pinning addresses it.
  • Workflows around Dependabot are part of the attack surface. Kong’s incident came from an actor confusion in a pull_request_target workflow, left on an old branch.
  • A bot’s name is not authentication. Forged commits and confused auto-merge both exploit it.
  • Dependabot-triggered runs have reduced rights, and Dependabot secrets decide how much that is worth.
  • Auto-merge removes the last check. It should be narrow, use the pull request author and stop on maintainer, script or attestation changes.
  • Review still matters. No control here replaces reading a bump to an important dependency, and volume works against that.

Mindmap of the dependency update path threat model covering trust boundaries, threats, public incidents, controls and auto-merge rules

Annex

Key Terms

Term Definition
Threat model A structured description of what can go wrong with a system and what is done about it.
Trust boundary A point where data or control passes between parties with different levels of trust.
STRIDE A set of threat categories: spoofing, tampering, repudiation, information disclosure, denial of service, elevation of privilege.
Supply-chain attack Compromising a dependency in order to reach its users.
Account takeover Gaining control of a maintainer’s account in order to publish under their name.
Install script Package code that runs during installation.
Cooldown A delay before a new release can be proposed by a version update.
Mutable reference A git tag or branch, which can be moved to another commit.
SHA pinning Referring to a dependency by its full commit hash.
Pwn Request An attack in which a pull request reaches a privileged workflow.
pull_request_target A GitHub Actions event that runs with the base repository’s rights.
Confused deputy A privileged component tricked into acting for someone who lacks the privilege.
Auto-merge workflow Automation that merges a pull request without human approval.
Dependabot secrets The only secrets available to workflows triggered by Dependabot.
Provenance attestation A signed statement of how and where a package was built.

Security Implementation Checklist

The rows below apply to a repository that uses automated dependency updates.

Configuration

Check Security requirement Failure mode if violated
☐ The cooldown stays enabled for version updates. A short-lived malicious release is proposed within minutes.
☐ GitHub Actions are pinned to full commit SHAs. A moved tag changes the code with no pull request.
☐ Lockfiles are committed. The version under review is not the version installed.
☐ Grouped updates keep risky packages separate. One approval covers a change nobody read.

CI

Check Security requirement Failure mode if violated
☐ Dependabot secrets hold only what tests need. Code under test reads a publishing token.
☐ CI installs without running scripts where the project allows it. A package acts at install time.
☐ No workflow runs pull request code under pull_request_target. Untrusted code runs with write access and secrets.
☐ Untrusted values such as branch names are passed through environment variables. A crafted value causes command injection.

Merging

Check Security requirement Failure mode if violated
☐ Auto-merge checks the pull request author, not github.actor. An outsider’s code is merged under Dependabot’s name.
☐ Auto-merge is limited to low-risk updates. A major change or a production dependency merges unread.
☐ Auto-merge stops on maintainer, script or attestation warnings. The warning that would have caught the release is ignored.
☐ Dependabot is not allowlisted for privileged workflows. Its name opens a path to secrets.
☐ Workflow fixes are applied to all branches, including old ones. An old branch keeps the vulnerable workflow.
☐ dependabot/** branches are protected if Dependabot has bypass rights. Someone else’s push acquires those rights.
☐ Security updates receive review despite their urgency. “Security fix” is treated as “safe”.

Trust

Check Security requirement Failure mode if violated
☐ A bot’s name is never treated as proof of origin. A forged commit is accepted.
☐ Tokens are fine-grained, and developers do not hold broad ones. One stolen token reaches many repositories.
☐ Text in the pull request is treated as untrusted, including by automated reviewers. A release note steers the decision.

Frequently Asked Questions

Q: What attack does a cooldown stop, and what does it miss?

It stops the most common pattern: a malicious release that is found and removed within hours or days. It misses backdoors that stay dormant for longer than the window, because waiting helps only if someone notices.

Q: Why are security updates exempt from the cooldown?

A delay would hold back a fix for a flaw that is already public, so the exemption is deliberate. The cost is that the fix release gets no waiting period, which is a reason to review it.

Q: Why is github.actor unsuitable for an auto-merge check?

It names whoever triggered the latest event, not the author of the pull request. If an outsider can cause Dependabot to act on a pull request, the actor can be Dependabot while the code came from someone else. The check should use the pull request author, and it should be part of a workflow that avoids pull_request_target.

Q: How did tj-actions reach users without any update?

The attackers moved existing version tags to a malicious commit. A workflow that used a tag ran the new code at its next run. No version number changed, so an update tool had nothing to report. A workflow pinned to a full commit SHA refers to fixed content.

Q: Are Dependabot pull requests safe to run in CI?

They run with reduced rights, since GitHub gives them a read-only token and only Dependabot secrets. They are not free of risk: new dependency code executes on the runner, so the risk depends on what those secrets can do.

Q: A team enables auto-merge for all Dependabot security updates, with an actor check. Combining the threats above, what is wrong?

Several things:

  • The actor check is vulnerable to T5, especially if the workflow uses pull_request_target.
  • Security updates skip the cooldown, so a malicious fix release gets no delay, as in T7.
  • The label “security” comes from the update’s purpose, and says nothing about the safety of the code.
  • Auto-merge removes the review that T2 and T8 rely on.

A safer version uses the author check, a narrow scope, and a human review for anything that affects production.

Gaps

The points below remain open.

Gap Why it matters What to read or verify
The likelihood and impact ratings are my judgement. They order the article’s advice, and no data supports them. Incident data for your own organisation.
T7 and T8 are reasoned from the design, with no known incident. They may be overstated. Future incident reports.
The “right-hand column” assessment of incidents is hindsight. A control may not have stopped a real attacker. Each incident’s full report.
The study of 21 incidents is cited through GitHub’s post. Its method was not read. The research linked from GitHub’s blog post.
Whether Shai-Hulud spread through any Dependabot pull request was not established. The table assigns T1 by pattern, not by a documented case. The vendors’ incident reports.
Package counts for Shai-Hulud vary across sources. The article gives the range only. GitHub’s own statement on the removal.
The behaviour of pin_to_sha? in the github_actions ecosystem was only skimmed. The article says only that SHA pins are handled. github_actions/lib/dependabot/github_actions/update_checker.rb.
Foundry support is inferred from the absence of an ecosystem and from how lib/ is usually set up. A project may manage dependencies differently. dependabot-core and the project’s foundry.toml.
The workflow rules on pull_request come from one research article. GitHub’s own guidance was not fully read. GitHub’s page on automating Dependabot with GitHub Actions.

References

Analyzed source

Incidents and research

GitHub

Methods

You might also enjoy