Safeguard
Supply Chain Security•

How to Patch an Open Source Dependency Without Waiting for Upstream

You do not have to wait for a maintainer. Override the version, apply a patch file, fork it, or pull a maintained patched build. Commands per ecosystem.

Safeguard Security Team
35 min read

You can patch a vulnerable open source dependency today, without waiting for the maintainer. If a fixed version exists anywhere, force it with your package manager's override feature. If no fix has shipped, apply a small patch file, point the build at a private fork, or pull a maintained patched build. Then record the patch so it is removed when upstream catches up.

Teams wait for upstream out of habit, not necessity. Every major ecosystem gives you a supported way to change what ends up in your build: npm overrides and patch files, pip constraints and local wheels, Maven dependency management, Go replace directives, vendoring, private forks, distribution backports and container rebuilds. Below are real commands for each, then the parts people skip: tests, scanners, licenses, upstreaming and cleanup.

Key takeaways

  • Waiting is optional. If a fixed release exists, an override in the root manifest forces it through every transitive path in minutes.
  • If no fixed release exists, a patch file (patch-package, pnpm patch, a Go replace to a local fork) changes the vulnerable code and survives every reinstall, as long as it is committed and applied in CI.
  • A fork you publish to a private registry is the most durable option, and also the one that rots fastest if nobody owns it.
  • Patched builds still look vulnerable to version based scanners. Publish a VEX statement, use a clear version label, and record the patch in your SBOM.
  • Modifying open source carries license duties, mostly light: keep notices, mark changed files, and know whether you distribute the result.
  • Send the fix upstream as soon as it is safe to do so. Every local patch is a debt with a removal date.
  • For end of life lines and packages you cannot staff, a maintained patched distribution is usually cheaper than owning the fork yourself.

What are your options when the maintainer has not fixed it?

There are seven distinct moves. They differ in how much code you change, how long the change lasts and who has to maintain it afterwards.

  1. Upgrade the direct dependency to a fixed version. Try this first.
  2. Override a version pinned somewhere in the middle of your tree.
  3. Patch in place by applying a diff to the installed package on every install.
  4. Fork the project, fix it and publish it to your own registry.
  5. Vendor the source into your repository and build it with your code.
  6. Backport a fix from a newer major line to the older line you run.
  7. Replace the dependency, or swap in a maintained hardened build of it.

Which option fits which situation?

SituationBest first moveHow long it lastsWho maintains it
Fixed version exists, direct dependencyUpgradePermanentUpstream
Fixed version exists, pinned by a transitive parentOverride or constraintUntil the parent updatesYou, lightly
Fix merged upstream but not releasedPatch file from the upstream commitUntil the next releaseYou, lightly
No upstream fix, small code changePatch file or Go replace to a local copyWeeks to monthsYou
No upstream fix, many projects affectedPrivate fork published to an internal registryMonthsYou, a named owner
Fix only exists on a newer major lineBackport to your line, or a maintained patched buildUntil you migrateYou or a distribution
Project abandoned or end of lifeReplace, or a maintained patched distributionIndefiniteA third party or you
Vulnerable code never called by youVEX statement, plus monitoringUntil your usage changesYou

The rest of this guide shows how to do each of these in the ecosystems people ask about most. If you need the narrower case of a transitive dependency with a fixed release, our guide to fixing a vulnerable transitive dependency in npm, pip, Maven and Gradle goes deeper on that one path, and why npm audit fix does not fix every vulnerability explains why the automatic route so often stalls.

Step by step: making the patch survive reinstalls and CI

The single most common failure with a local fix is that it works on one laptop and vanishes on the next npm ci. A patch only counts if it lives in version control and is applied by the same command your CI runs. Everything below follows that rule.

npm: overrides for a fixed version

The npm documentation describes overrides as the way to replace a package anywhere in your tree with a different version or a different package. Three details matter.

First, overrides are only read from the root package.json. In the docs' words, "Overrides are only considered in the root package.json file for a project." Overrides inside your dependencies or workspaces are ignored.

Second, you can scope an override. A plain entry applies everywhere; a nested entry applies only beneath a given parent, and a key like "parent@2.0.0" applies only to that version of the parent.

Third, you cannot override a direct dependency to a spec that differs from what you declared. npm throws EOVERRIDE. The fix is the $ reference, which reuses the spec of your direct dependency.

{
  "dependencies": {
    "minimist": "^1.2.8"
  },
  "overrides": {
    "minimist": "$minimist",
    "some-cli-tool": {
      "semver": "7.5.4"
    }
  }
}

Then reinstall and confirm what actually resolved:

rm -rf node_modules package-lock.json
npm install
npm ls semver --all
npm ci && npm test

Commit the regenerated package-lock.json; the lockfile is the proof.

Yarn uses a resolutions field for the same purpose. Its documentation says resolutions can "backport a fix" and that the field "can only be set at the root of the project." Keys can target a package by name or by parent path such as @babel/core/json5.

{
  "resolutions": {
    "@babel/core/json5": "2.2.3"
  }
}

npm: patch-package when no fixed version exists

When there is nothing to override to, you change the code itself. The patch-package tool records the diff between the published package and your edited copy in node_modules, then reapplies it after every install.

npm i --save-dev patch-package

# edit the vulnerable file under node_modules/some-lib/
npx patch-package some-lib
git add patches/

Add the postinstall hook so every install reapplies the patch:

{
  "scripts": {
    "postinstall": "patch-package"
  }
}

The patch lands in patches/ with the version in its filename, for example some-lib+3.4.1.patch. That naming is a feature: when the package version changes, patch-package tells you, so you notice that your fix may no longer apply or may no longer be needed.

Four habits keep this honest in CI:

  • The project notes that --error-on-fail makes it exit with code 1 when a patch fails, and that this is on by default on CI. Do not turn it off. A silently skipped security patch is worse than a failed build.
  • In Docker builds, copy the patches/ directory into the image before running the install step, or the postinstall hook runs with nothing to apply.
  • Include the patches directory in your CI cache key, as the project recommends, so an updated patch is not masked by a cached node_modules.
  • For nested packages, the path separator is /, as in npx patch-package parent/child.

pnpm and Yarn: native patching

pnpm and modern Yarn ship their own patch commands, so you do not need an extra tool. With pnpm:

pnpm patch some-lib@3.4.1
# pnpm prints a temporary directory; edit the files there
pnpm patch-commit /path/printed/above

pnpm patch-commit writes a file such as patches/some-lib@3.4.1.patch and registers it under patchedDependencies. pnpm's documentation is explicit that you should not use pnpm patch to change a package's own dependencies; use overrides or a pnpmfile hook for that. It also notes that by default installation fails if a listed patch was not applied (allowUnusedPatches defaults to false), which is exactly the behaviour you want for a security fix.

Python: constraints, overrides and local wheels

pip does not have an override in the npm sense. What it has is a constraints file, and the pip user guide is clear about what that does: constraints "only control which version of a requirement is installed," and listing a package there "does not trigger installation of the package."

cat > constraints.txt <<'EOF'
urllib3>=2.2.2
jinja2>=3.1.4
EOF

python -m pip install -r requirements.txt -c constraints.txt
python -m pip show urllib3

pip's guide also notes that from pip 26.2, constraints files, including those set with PIP_CONSTRAINT, "no longer affect isolated build environments." If the vulnerable package is a build dependency, use --build-constraint instead.

Constraints can only narrow. If a parent package declares an upper bound that excludes the fixed version, you need an override. If you use uv, its documentation describes override-dependencies, which "can expand the set of acceptable versions," either globally or scoped to the dependencies of one package version:

[tool.uv]
override-dependencies = [
    "urllib3>=2.2.2",
    { package = { name = "old-sdk", version = "1.9.0" }, dependencies = ["urllib3>=2.2.2"] },
]

When no fixed release exists on the index, build your own wheel from a patched checkout and install it from a local directory:

git clone https://github.com/example/somelib.git
cd somelib
git checkout v1.8.3
git apply ../fixes/somelib-cve-fix.patch
# set the version to 1.8.3+acme.1 in pyproject.toml (see the versioning section below)
python -m pip wheel --wheel-dir ../wheelhouse .

cd ..
python -m pip install --no-index --find-links=./wheelhouse somelib==1.8.3+acme.1

For a team, publish that wheel to your internal package index rather than passing wheel files around, and pin it with a hash in your lockfile. pip's guide notes that hash checking "requires that all requirements are specified as a == match on a version," which suits a pinned patched build well.

Maven: dependencyManagement and exclusions

Maven picks transitive versions by "nearest definition," which is why an old vulnerable library can win even after you thought you upgraded. The Maven documentation states that dependency management takes precedence over that mediation for transitive dependencies. A version you set in dependencyManagement wins, wherever the library sits in the tree.

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.17.2</version>
    </dependency>
  </dependencies>
</dependencyManagement>

Confirm the result rather than trusting it:

mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind
mvn -q verify

Two cautions from the same documentation. Dependency management does not affect the dependencies of Maven plugins, so a vulnerable library pulled in by a plugin needs a plugin level fix. And when several imported BOMs manage the same artifact, the first declared import wins, so put your security pins in your own POM rather than relying on BOM order.

If the vulnerable library is not needed at all, exclude it:

<dependency>
  <groupId>org.example</groupId>
  <artifactId>reporting-kit</artifactId>
  <version>4.2.0</version>
  <exclusions>
    <exclusion>
      <groupId>org.example</groupId>
      <artifactId>legacy-xml-parser</artifactId>
    </exclusion>
  </exclusions>
</dependency>

When no fixed Maven release exists, build the patched artifact from source with a distinguishing version such as 2.9.10-acme.1, deploy it to your internal repository with mvn deploy, and reference that version in dependencyManagement.

Gradle: constraints with a reason

Gradle's equivalent is a dependency constraint. Its documentation explains that a constraint sets a version requirement without adding the module, takes effect only when something pulls the module in, and records your rationale with because. That rationale is useful later, when someone wonders why the pin exists.

dependencies {
    implementation("org.example:reporting-kit:4.2.0")

    constraints {
        implementation("org.yaml:snakeyaml:2.2") {
            because("CVE fix; remove when reporting-kit ships 4.3")
        }
    }
}

Constraints are "at least this version" by default. If you need to hold an exact patched version, use a strict version declaration, and check the outcome with ./gradlew dependencyInsight --dependency snakeyaml.

Go: replace directives

Go's replace directive swaps one module for another, either a different module path and version or a local directory. The Go modules reference gives this shape:

replace golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
replace golang.org/x/net => ./third_party/net

From the command line:

go mod edit -replace=github.com/example/parser=github.com/acme/parser@v1.4.2-acme.1
go mod tidy
go build ./... && go test ./...
go list -m all | grep parser

Three rules from the reference trip people up:

  • replace only applies in the main module's go.mod (or a go.work file). It is ignored in the go.mod of your dependencies. If you publish a library with a replace in it, your consumers do not get your fix.
  • A local path replacement must contain its own go.mod, and its module line must match the path being replaced.
  • If you put a version on the left side, only that version is replaced. Leave it off to replace all versions.

Vendoring

Vendoring copies dependency source into your repository so the build does not fetch it at all. In Go, go mod vendor creates a vendor directory and a vendor/modules.txt file, and the toolchain uses the vendor directory automatically when the module's go version is 1.14 or later. You can then patch the vendored code directly.

Be careful: the next go mod vendor overwrites hand edits, and Go errors if vendor/modules.txt drifts from go.mod. The safer pattern is a replace pointing at a patched copy under third_party/, which vendoring then copies faithfully. In any ecosystem, a vendored copy edited by hand with no patch file is the hardest fix to find or remove later.

Private forks

A fork suits a fix that several services need, that is more than a few lines, or that you will carry for months:

  1. Fork the repository into your organisation and create a branch from the exact tag you run, not from the default branch.
  2. Apply the fix as a small, separate commit with the CVE or advisory ID in the message.
  3. Bump the version with a clear local label (see the versioning section below).
  4. Build in CI with the project's own test suite, plus your regression test for the vulnerability.
  5. Publish to your internal registry: a scoped npm package such as @acme/somelib, a wheel on your internal index, a Maven artifact in your repository manager, or a tag on your Go module fork.
  6. Point consumers at it with the override, constraint, dependencyManagement entry or replace shown above.

Our post on when to fork an abandoned dependency covers the decision itself in more depth.

Distribution backports

Much of your exposure sits in operating system packages: OpenSSL, glibc, zlib, curl, language runtimes installed with apt or dnf. Here you usually should not patch at all, because the distribution already backports security fixes. Debian's security FAQ explains the policy: instead of moving to a new upstream release, "we backport security fixes to the version that was shipped in the stable release." Red Hat describes the same practice and gives the example of a PHP 5.3 fix backported after that version's upstream end of life, so customers could stay on 5.3 and still be protected.

Check whether a backport has landed before you build anything:

# Debian and Ubuntu
apt-get update && apt-cache policy openssl
apt changelog openssl | grep -i CVE-

# RHEL, Rocky, Alma, Fedora
dnf updateinfo list --security
rpm -q --changelog openssl | grep -i CVE-

Debian's FAQ points to its Security Tracker, where "you can check the state of every single CVE ID." That is the source of truth for whether a Debian package version is fixed, regardless of how old its upstream version number looks.

Container base image rebuilds

For containers, the fastest patch is often a rebuild. A base image tag like debian:bookworm-slim is rebuilt by its publisher as distribution fixes land, but your image keeps whatever layer it was built from until you rebuild it. Docker's build reference describes --pull as "Always attempt to pull all referenced images" and --no-cache as "Do not use cache when building the image."

docker buildx build --pull --no-cache -t registry.example.com/api:2026-10-22 .

If the publisher has not rebuilt yet but the distribution has shipped the fix, apply it in your own Dockerfile:

FROM debian:bookworm-slim
RUN apt-get update \
 && apt-get install -y --only-upgrade openssl libssl3 \
 && rm -rf /var/lib/apt/lists/*

Then pin the base by digest and schedule rebuilds; a weekly --pull rebuild closes most distribution level findings with no code change.

How do you backport a fix to an older major version safely?

Backporting means taking a fix written for version 5 and making it work on the version 3 you still run. It is where most self made patches go wrong, because the code around the fix has moved.

A safe sequence looks like this:

  1. Find the real fix. Read the advisory's references for the fixing commit or pull request. If the advisory only names a fixed version, compare the tags: git log --oneline v5.1.0..v5.1.1 usually shows a handful of commits, and the security fix is often labelled.
  2. Write the failing test first. Before you touch the code, write a test on your old branch that demonstrates the unsafe behaviour in a harmless way: an input that should be rejected and is not, a path that should be normalised and is not, a length check that is missing. Run it and watch it fail. This is your proof, and it is the only way to show a reviewer the patch does what it claims. Keep it descriptive and benign; it should assert the safe behaviour, not carry a weaponised payload.
  3. Port the change by hand, not by force. Try git cherry-pick -x <commit> first. The -x flag records the original commit hash in the message, which helps later. If the cherry pick conflicts, port the logic manually and keep the diff as small as you can. Resist fixing unrelated things in the same commit.
  4. Run the project's full suite on the old line, then your new test. Both must pass. If the old line has no CI, set one up on the fork before you trust the build.
  5. Run your own integration tests against the patched build. Security fixes often tighten input handling and can reject data you used to send.
  6. Have a second reviewer compare the upstream fix and your port side by side. The classic backport bug misses a call site the newer version had refactored away.
  7. Record what you did in the commit message, the changelog and the SBOM (covered below).

Distributions follow the same discipline at scale. Red Hat's description of backporting stresses isolating the fix from other changes and checking for side effects before shipping. That is the bar to hold yourself to.

AI assistants can speed up steps 1 to 3, but in a recent Hacker News discussion one commenter noted that an assistant "can't actually upgrade and release packages for discontinued distro release streams, that's a human problem." Writing the diff is the easy part; testing, publishing and maintaining it is the work.

How do you keep scanners from flagging a patched build as still vulnerable?

Most software composition scanners decide "vulnerable or not" by comparing a package name and version against an advisory's affected range. Patch somelib 1.8.3 in place and the scanner still sees somelib 1.8.3, still inside the affected range, and still raises the finding. Red Hat names this exact problem for backports: version based scanners "produce false positives" because a package may look outdated while already containing the fix.

You have three tools to fix the signal. Use all of them.

1. Version labels that say "patched"

Give your build a version that is clearly yours and still sorts correctly.

  • Python has a built in mechanism. PEP 440 local version identifiers, written 1.8.3+acme.1, exist for downstream patched builds. The PEP says they are meant for integrators who backport fixes, that a local version sorts above the same version without a label, and that the public Python Package Index "MUST NOT allow the use of local version identifiers." They are for your private index only.
  • npm and other SemVer ecosystems need care. Under the SemVer specification, "Pre-release versions have a lower precedence than the associated normal version," so 1.8.3-acme.1 sorts below 1.8.3 and a range like ^1.8.3 will not select it. Build metadata such as 1.8.3+acme.1 is ignored for precedence entirely. The cleaner route is a scoped package name (@acme/somelib) with a version you control, or a version bump beyond upstream that you can explain.
  • Maven has no formal rule, but a suffix such as 2.9.10-acme.1 in your internal repository is common and readable. Check how your scanner parses it before you depend on it.
  • Go forks get a new module path or a tag on your fork, so the scanner sees a different module altogether.

A new name or version stops the false match but can also hide the package from future advisories, so pair it with the next two steps.

2. VEX statements

VEX (Vulnerability Exploitability eXchange) is a machine readable statement about whether a product is affected by a given vulnerability. The OpenVEX specification defines four statuses: not_affected, affected, fixed and under_investigation. A not_affected statement must carry a justification, from a fixed list: component_not_present, vulnerable_code_not_present, vulnerable_code_not_in_execute_path, vulnerable_code_cannot_be_controlled_by_adversary or inline_mitigations_already_exist.

For a patched build, fixed against your product version, or not_affected with vulnerable_code_not_present, tells scanners and customers exactly what you did:

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://example.com/vex/api-2026-10-22",
  "author": "Acme Security Team",
  "timestamp": "2026-10-22T09:00:00Z",
  "version": 1,
  "statements": [
    {
      "vulnerability": { "name": "CVE-2026-00000" },
      "products": [
        {
          "@id": "pkg:oci/api@sha256:3f1c...",
          "subcomponents": [ { "@id": "pkg:pypi/somelib@1.8.3%2Bacme.1" } ]
        }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_present"
    }
  ]
}

The CVE ID above is a placeholder. Write VEX statements only when you can defend them; a not_affected claim you cannot back up is worse than an open finding.

3. SBOM pedigree

CycloneDX lets an SBOM record where a component came from and what was changed. Its pedigree section holds ancestors (the upstream component you derived from), commits, and patches. Each patch has a type, such as backport or unofficial, an optional diff, and a resolves list that links it to the vulnerability it fixes.

{
  "type": "library",
  "name": "somelib",
  "version": "1.8.3+acme.1",
  "pedigree": {
    "ancestors": [ { "type": "library", "name": "somelib", "version": "1.8.3" } ],
    "patches": [
      {
        "type": "backport",
        "resolves": [ { "type": "security", "id": "CVE-2026-00000" } ]
      }
    ]
  }
}

Anyone reading the SBOM later can see the upstream ancestor, the patch and the vulnerability it fixes.

What are the licensing and compliance obligations when you modify open source?

Almost every license allows patching. Obligations depend on the license and on whether you distribute the result. This is general information, not legal advice.

License familyCan you patch it?Main duties if you distribute the patched version
Permissive (MIT, BSD)YesKeep the copyright and license notice
Apache 2.0YesKeep notices, include the license, mark modified files, carry the NOTICE file
Weak copyleft (LGPL, MPL)YesMake the source of the modified library or modified files available under the same license
Strong copyleft (GPL)YesProvide corresponding source of the modified program to recipients under the GPL
Network copyleft (AGPL)YesUsers who interact with a modified version over a network can be entitled to its source

Apache 2.0 is worth reading closely because it is so common. Section 4 requires that "You must cause any modified files to carry prominent notices stating that You changed the files," that you give recipients a copy of the license, retain copyright notices, and include a readable copy of the attribution notices in any NOTICE file. A one line comment at the top of each changed file, plus a PATCHES.md in the fork, covers the first duty.

The practical rules:

  • Internal use is the easy case. Under most licenses, duties attach when you distribute. A patched library used only inside your own backend usually triggers little beyond keeping notices. AGPL is the main exception, because network interaction can count.
  • Shipping software changes the picture. If you ship a desktop app, a mobile app, an appliance or a container image to customers, you are distributing the patched component, and the license's distribution terms apply.
  • Keep the license file in the fork. Never strip LICENSE, NOTICE or copyright headers while trimming a package.
  • Record the change in your SBOM with the pedigree data above, so your license review sees a modified component rather than a pristine one.

When should you send the patch upstream, and how?

As soon as it is safe, which usually means as soon as the vulnerability is public or the maintainer agrees on timing. Every local patch is code you must reapply, retest and remove; landing it upstream removes that cost for everyone.

The order of operations depends on whether the vulnerability is already public.

If the vulnerability is not yet public, do not open a public issue or pull request that describes it. Use the project's private channel. GitHub's documentation explains that on repositories with private vulnerability reporting enabled, you open the "Security and quality" tab and choose "Report a vulnerability," and you can start a temporary private fork to work on the fix with the maintainers. If private reporting is not enabled, GitHub says to follow the instructions in the repository's security policy, or open an issue asking only for a preferred security contact. Read SECURITY.md first; many projects list an email address.

If the vulnerability is already public, open a normal pull request against the default branch with:

  • the advisory or CVE ID in the title or description,
  • the smallest diff that fixes it,
  • a regression test,
  • a short note on which release lines are affected, and whether you have a backport ready for an older line.

Offer any backport as a separate pull request against the maintenance branch; maintainers often welcome a tested one.

Then keep your local patch until a release that contains the fix is published, not merely merged. Merged is not released, and released is not in your lockfile. Our guide to submitting an upstream fix for a shared dependency inside your organisation covers the coordination side when several teams depend on the same library.

When is a maintained patched distribution better than patching it yourself?

Doing it yourself makes sense for a small, well understood fix in a package you know, carried for weeks until upstream releases. It stops making sense when any of these are true:

  • The line is end of life. The Node.js project states that end of life versions "will no longer receive updates, including security patches." Node.js 20 reached end of life on 30 April 2026 according to the project's release schedule, and the project lists commercial extended support programmes while saying that commercial support "should be viewed as a temporary solution." If you are on an end of life line, you will be backporting every future fix too, not just this one.
  • The fix touches cryptography, parsing or native code, where a subtle backport bug costs most.
  • Many teams consume it. Ten patch files to keep in sync versus one maintained build.
  • You need evidence. Regulated buyers ask for provenance and a record of changes per component; a maintained build can supply both.
  • Nobody owns the fork, which is how forks become the next vulnerability.
FactorPatch it yourselfMaintained patched distribution
Time to first fixHours, if you have the skillsDepends on catalogue coverage
Ongoing costEvery future fix on that line is yoursShared across the provider's users
Testing depthYour suite plus the project'sProvider's compatibility testing plus yours
Evidence for auditorsYou produce itUsually supplied per artifact
Control over the diffTotalLimited to what the provider ships
Best forSmall, short lived fixesEnd of life lines, native code, many consumers

Our comparison of upstream, vendor or managed sources for patched open source goes deeper on choosing between them.

How do you track all your local patches so they get removed when upstream catches up?

Every patch, override and fork should have an owner, a reason and an exit condition. Without those, they pile up, and two years later an override is forcing an old version that now has its own vulnerabilities.

A lightweight register is enough. Keep a PATCHES.md (or a YAML file your CI can read) at the repository root:

- id: somelib-cve-2026-00000
  kind: patch-file            # override | patch-file | fork | replace | vendored
  package: somelib
  pinned_or_patched: 1.8.3+acme.1
  reason: CVE-2026-00000, path traversal in archive extraction
  upstream: https://github.com/example/somelib/pull/1234
  owner: platform-security
  added: 2026-10-22
  remove_when: somelib >= 1.8.4 released and adopted

Then automate the exit. Let patch failures break the build (patch-package's --error-on-fail, pnpm's default). Run a small CI job with npm outdated or pip index versions somelib that flags entries whose remove_when condition is met. Use Gradle's because and comments beside Maven pins. Inventory everything with git grep -n "overrides\|resolutions\|patchedDependencies\|replace ", and review the register monthly like any other security exception.

The SBOM pedigree data from earlier ties this together: a patched component in your SBOM points back to its upstream ancestor and the fix it carries, so you can query which products still ship a local patch.

Common mistakes

  • Patching node_modules by hand and not saving the diff. It works until the next clean install. Always generate a patch file and commit it.
  • Putting overrides in a workspace package. npm and Yarn only read them from the root manifest. The override silently does nothing.
  • Publishing a library with a Go replace and assuming consumers get the fix. Replace directives in dependencies are ignored.
  • Using a SemVer prerelease suffix for a patched build. 1.2.3-patched sorts below 1.2.3, so a caret range picks the unpatched release.
  • Trusting the version string on a distribution package. Backported fixes keep old version numbers. Check the changelog or the distribution's security tracker.
  • Skipping the regression test. Without a test that failed before and passes after, you have a diff, not a fix.
  • Writing not_affected VEX statements you cannot defend. VEX is a claim. Make it only with evidence.
  • Disabling patch failure checks to get a build green. A security patch that silently fails to apply is the worst outcome on this list.

Fix it without waiting for upstream

Everything above is the do it yourself route, and it works. The trade off is that every override, patch file and fork becomes code your team owns. Safeguard exists so you can take the same shortcuts without carrying all of that weight alone.

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.

3. Gold Registry: start from components that are already fixed

registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

How this maps to the do it yourself steps

Look up the safe version on Gold Open Source before you write an override or constraint. Let Griffin open the upgrade pull request when a fixed version exists, once you switch remediation on for the categories you choose. Add the Gold scan action to the workflow that runs your tests so a removed version cannot return; check the action's README for its current inputs. Scan repositories, images and SBOM files with the Safeguard CLI (safeguard scan --dir .) to find every project that carries the vulnerable version before you patch. For a package many teams use, or a line upstream no longer fixes, pull the hardened equivalent from Gold Registry where one exists.

FAQ

Can I patch an npm package without forking it?

Yes. Use patch-package (or pnpm patch and yarn patch if you use those managers) to record a diff against the installed package, commit the file under patches/, and add a postinstall hook so it is reapplied on every install. If a fixed version already exists, an overrides entry in the root package.json is simpler still.

How do I force a transitive dependency to a newer version?

In npm, use overrides; in Yarn, resolutions; in pnpm, its overrides setting. In Maven, set the version in dependencyManagement, which takes precedence over nearest definition. In Gradle, add a dependency constraint. In pip, use a constraints file, or uv's override-dependencies if a parent's upper bound is in the way. In Go, use replace or simply require the newer version in your own go.mod.

Why does my scanner still flag the package after I patched it?

Because most scanners match on name and version, and an in place patch does not change the version. Give the build a distinguishing version (a PEP 440 local label in Python, a scoped name or controlled version elsewhere), publish a VEX statement, and record the patch in your SBOM's pedigree data.

Is it legal to modify an open source library?

Yes, virtually every open source license permits modification. The obligations are about distribution: keep notices, mark changed files under Apache 2.0, and provide source for modified copyleft components when you distribute them. AGPL can apply to network use. Check unusual cases with counsel.

Do Go replace directives work in libraries?

No. Replace directives are only honoured in the main module's go.mod or a go.work file. Consumers of your library ignore them, so a library cannot pass a fix to its users through replace. Publish a fixed version or ask consumers to add their own replace.

What is the safest way to backport a security fix?

Find the upstream fixing commit, write a regression test that fails on your current version, cherry pick or port the change with the smallest possible diff, run the full project suite and your integration tests, and have a second person review the port against the original. Record it in the changelog and SBOM.

Why does my Debian or Red Hat package show an old version if it is patched?

Both distributions backport security fixes into the version they shipped, rather than moving to a new upstream release, to avoid breaking changes. Check the package changelog or the distribution's security tracker to confirm a fix, not the upstream version number.

How do I patch a vulnerability inside a container base image?

Rebuild with docker buildx build --pull --no-cache to pick up the publisher's latest base layer. If the base has not been rebuilt yet but the distribution package is fixed, upgrade that package in your Dockerfile with apt-get install --only-upgrade or dnf upgrade, then pin the base by digest and schedule regular rebuilds.

See how Safeguard would help

This first demo shows the Safeguard CLI fixing a lodash finding in a real repository. The assistant reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. The same approve each step flow applies when you want an override, a version bump or a test run done for you without giving up control.

Click through it at your own pace, or open it full screen.

The second demo connects Claude to the Safeguard MCP server with OAuth and shows how you choose which tools Claude can use. That is useful when you want an assistant to look up which projects carry a vulnerable version, or what the fixed version is, before anyone writes a patch.

Click through it at your own pace, or open it full screen.

The third demo sets up the Safeguard runner on your own machine, approves it with its device code, then scans from the terminal. Running the scan locally is a quick way to confirm that an override or patched build actually changed what resolves, before you open the pull request.

Click through it at your own pace, or open it full screen.

To try it on your own code, download Safeguard or read the CLI documentation.

Quick check

You added an npm override for a vulnerable transitive package in a workspace's package.json, and nothing changed. Why?

npm only reads overrides from the root package.json of the project. Overrides in workspaces or installed dependencies are ignored, so move the entry to the root manifest and reinstall.

You patched somelib 1.8.3 in place and your scanner still reports the CVE. What three things fix the signal?

Give the patched build a distinguishing version (for example a PEP 440 local label such as 1.8.3+acme.1 on a private index), publish a VEX statement saying the product is fixed or not affected with a justification, and record the patch in your SBOM's pedigree data.

Sources

Last reviewed: 22 October 2026. Package manager behaviour changes between releases; check the linked documentation for your version.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.