Safeguard
Supply Chain Security•

Backport the Fix: Most 2026 Security Fixes Reach Only One Release Line

Only 15% of 2026 GitHub advisory fixes reach more than one major line. What that means for KEDA, Better Auth and DOMPurify 2.x users, plus a check.

Safeguard Security Team
Security Research
10 min read

A security fix that ships only on the newest release line isn't a fix for everyone on the older ones. In 2026, that's the normal case. Across 15,496 package entries in the 10,855 advisories GitHub reviewed between 1 January and 15 October, only 15.2% of those with a fix list fixed versions on more than one major release line. If you're a major version behind, most advisories offer no fix on your line.

This post is for maintainers deciding how to ship security fixes, and for engineering teams deciding which release lines they can safely sit on. It sets out the data, three cases from this month, and what each side should do. It includes a script that finds npm dependencies whose current major line has no fix for their known vulnerabilities.

The short answer

Maintainers should publish which release lines get security fixes, for how long, and record every backport in the advisory. If a fix can't be backported, the advisory should say so, and should say whether the older line is affected at all. Consumers should treat "fixed only on a newer major" as its own risk, distinct from "fixed", and track whether each important dependency's current line is still supported. A version scanner reporting "upgrade to 3.4.0" isn't telling you that 2.x will never get the fix.

What the 2026 advisories show

We took every GitHub-reviewed advisory published from 1 January to 15 October 2026 in OSV's exports for eight ecosystems. For each package entry, we classed the fixed versions it lists. Here "major line" means the first version number, or 0.y for versions below 1.0.

EcosystemAdvisory-package entriesNo fixed version listedFixes on 2+ major lines (share of fixed)
npm3,5176.3%19.2%
PyPI2,3069.6%5.9%
NuGet3,2860.1%13.7%
Go2,43915.1%5.2%
Packagist1,51616.0%31.6%
Maven1,3128.9%29.8%
crates.io9287.4%4.8%
RubyGems1922.1%30.9%
All15,4968.0%15.2%
High or critical only6,9279.3%18.7%

Backports are more common in ecosystems with large, long-lived frameworks. Both include large frameworks with published support windows, such as Drupal and Symfony on Packagist and Spring on Maven. PyPI and crates.io are at the other end: fewer than 1 in 16 fixes reach a second major line. Go's figure is low partly by design, because a Go module's v2 and later are separate module paths with their own advisory entries.

The numbers have a limit. An advisory listing one fixed version doesn't prove the older line is affected. GitHub's ranges often start at 0, which means "every earlier version", and maintainers rarely check whether a bug exists in a line they no longer touch. But a scanner has nothing better to go on. If the advisory says 2.x is affected and lists no 2.x fix, every tool will report 2.x users as exposed with no patch available.

Three cases from this month

KEDA, CVE-2026-77524. A critical privilege escalation in the Kubernetes autoscaler was fixed only in 2.21.0 on 23 September. That release has three breaking changes, one of them the security fix itself. There's no 2.20.x patch. Teams that hold back minor releases for compatibility have to take the migration or stay exposed.

Better Auth, CVE-2026-110862. A critical account takeover in the TypeScript auth library was fixed in 1.7.7 on 30 September. The affected range goes back to 1.4.0-beta.18, and the 1.6 and 1.4 lines got no fixed release. In the week of 6 to 12 October, 80% of the package's npm downloads were affected versions.

DOMPurify 2.x. DOMPurify's README tells users who need Internet Explorer compatibility to use the 2.x branch "for important security updates". The branch is still active, with dependency updates as recently as 10 October. Its last npm release, though, is 2.5.9 from 5 March 2026. Since then, GitHub has reviewed 20 DOMPurify advisories. All 20 point only to fixes on 3.x, and 16 of them mark 2.x as affected. In the most recent week npm reported on 16 October, 2.x versions had 2,716,071 downloads, 3.8% of the package's total. Some of those bugs may not exist in 2.x. Nothing in the advisory data says either way.

None of these maintainers did anything unusual. The point is that it's normal, and that the people on older lines often don't know.

What maintainers can do

  1. Publish a support policy. Say which lines get security fixes and until when. Node.js, Django and Atlassian publish them, and consumers plan around them. A README sentence is better than nothing. A table with end dates is better still.
  2. Record backports in the advisory. If you ship 2.5.10 with the fix, list it as a fixed version. Otherwise every scanner will tell 2.x users there's no patch.
  3. Say when an older line isn't affected. Narrow the range rather than leaving it at 0. It takes a few minutes and saves every downstream team from guessing.
  4. Keep the security fix separable from breaking changes. When the fix needs a breaking change, as KEDA's did, consider a smaller mitigation for the old line, or a short window of support for it, and say so in the advisory.

What consumers can do

  1. Know your lines. For each dependency that matters, know which major you're on and whether that line is still supported.
  2. Treat cross-major fixes as their own class. "Fixed in 3.4.0" means something different when you're on 2.x. Plan that upgrade like a migration, with an owner and a date.
  3. Check what the advisory actually covers. If you're on an older line, read whether the maintainer tested it. If they didn't, it may be worth asking.
  4. Find stranded dependencies. The script below does this for npm.

Find dependencies stranded on an unfixed line

This script reads package-lock.json (format 2 or 3), asks OSV about each package version, and lists the ones with vulnerabilities fixed only on a different major line. For each, it shows when your line last had a release. It uses only the Python standard library, and sends package names and versions to OSV and the npm registry.

#!/usr/bin/env python3
"""List npm dependencies whose known vulnerabilities are fixed only on a newer
major line, so staying on your current major leaves them unfixed.

  python3 stranded_lines.py package-lock.json

Reads package-lock.json (v2/v3), asks OSV for each name@version, then checks
whether every fixed version OSV lists is on a different major than yours. For
those packages it also shows when your major line last had a release.
Standard library only; sends package names and versions to api.osv.dev and
registry.npmjs.org.
"""
import json, re, sys, urllib.request, datetime as dt

OSV = "https://api.osv.dev/v1"


def post(url, body):
    req = urllib.request.Request(url, json.dumps(body).encode(), {"Content-Type": "application/json"})
    return json.load(urllib.request.urlopen(req, timeout=30))


def get(url):
    return json.load(urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "stranded/1.0"}), timeout=30))


def major(v):
    m = re.match(r"(\d+)\.(\d+)", v)
    if not m:
        return None
    a, b = int(m.group(1)), int(m.group(2))
    return (a,) if a >= 1 else (0, b)  # 0.x minors are separate lines under semver


def main(path):
    lock = json.load(open(path))
    deps = {}
    for key, meta in lock.get("packages", {}).items():
        if key and "version" in meta:
            name = meta.get("name") or key.split("node_modules/")[-1]
            deps[(name, meta["version"])] = True
    pairs = sorted(deps)
    queries = [{"package": {"ecosystem": "npm", "name": n}, "version": v} for n, v in pairs]
    results = []
    for i in range(0, len(queries), 500):
        results += post(f"{OSV}/querybatch", {"queries": queries[i:i + 500]})["results"]
    stranded = 0
    for (name, version), res in zip(pairs, results):
        ids = [v["id"] for v in res.get("vulns", [])]
        if not ids:
            continue
        mine = major(version)
        no_fix, other_line, same_line = [], [], []
        for vid in ids:
            if vid.startswith("MAL-"):
                continue
            vuln = get(f"{OSV}/vulns/{vid}")
            events = [e for a in vuln.get("affected", []) if a["package"]["name"] == name
                      for r in a.get("ranges", []) for e in r.get("events", [])]
            # "last_affected: X" means the next release after X on that line is fixed
            fixed = [e["fixed"] for e in events if "fixed" in e] + \
                    [f"after {e['last_affected']}" for e in events if "last_affected" in e]
            if not fixed:
                no_fix.append(vid)
            elif any(major(f.removeprefix("after ")) == mine for f in fixed):
                same_line.append(vid)
            else:
                other_line.append((vid, sorted(set(fixed))))
        if not (other_line or no_fix):
            continue
        stranded += 1
        times = get(f"https://registry.npmjs.org/{name.replace('/', '%2F')}")["time"]
        line = [(t, v) for v, t in times.items() if major(v) == mine and re.fullmatch(r"[\d.]+", v)]
        last_t, last_v = max(line) if line else ("?", "?")
        age = (dt.datetime.now(dt.timezone.utc) - dt.datetime.fromisoformat(last_t.replace("Z", "+00:00"))).days if line else "?"
        print(f"{name}@{version} (line {'.'.join(map(str, mine))}.x, last release {last_v} {age} days ago)")
        for vid, fixed in other_line:
            print(f"  {vid}: fixed only on another line: {', '.join(fixed)}")
        for vid in no_fix:
            print(f"  {vid}: no fixed version listed")
        if same_line:
            print(f"  (+{len(same_line)} fixed within your line: upgrade within the line for those)")
    print(f"\n{len(pairs)} packages checked, {stranded} with vulnerabilities your major line has no fix for")
    sys.exit(1 if stranded else 0)


if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.exit(__doc__)
    main(sys.argv[1])

We ran it on 16 October against a fixture lockfile with DOMPurify 2.5.9, Express 4.21.2, lodash 4.17.21 and Better Auth 1.6.33:

dompurify@2.5.9 (line 2.x, last release 2.5.9 224 days ago)
  GHSA-39q2-94rc-95cp: fixed only on another line: 3.4.0
  GHSA-55q2-fjhq-7xh7: fixed only on another line: 3.4.13
  GHSA-6688-9rhm-gjv2: fixed only on another line: 3.4.16
  GHSA-76mc-f452-cxcm: fixed only on another line: 3.4.7
  GHSA-c2j3-45gr-mqc4: fixed only on another line: 3.4.12
  GHSA-cj63-jhhr-wcxv: fixed only on another line: 3.3.2
  GHSA-cjmm-f4jc-qw8r: fixed only on another line: 3.3.2
  GHSA-cmwh-pvxp-8882: fixed only on another line: 3.4.11
  GHSA-crv5-9vww-q3g8: fixed only on another line: 3.4.0
  GHSA-h7mw-gpvr-xq4m: fixed only on another line: 3.4.0
  GHSA-h8r8-wccr-v5f2: fixed only on another line: 3.3.2
  GHSA-hpcv-96wg-7vj8: fixed only on another line: 3.4.6
  GHSA-r47g-fvhr-h676: fixed only on another line: 3.4.6
  GHSA-rp9w-3fw7-7cwq: fixed only on another line: 3.4.7
  GHSA-vhxf-7vqr-mrjg: fixed only on another line: 3.2.4
  GHSA-vxr8-fq34-vvx9: fixed only on another line: 3.4.9
  GHSA-x4vx-rjvf-j5p4: fixed only on another line: after 3.4.6

4 packages checked, 1 with vulnerabilities your major line has no fix for
exit 1

Express and lodash aren't listed, because their known fixes are on the lines in use. Better Auth 1.6.33 isn't listed either, but that's a gap in the data, not good news. On 15 October OSV had no record of CVE-2026-110862, which affects it. A tool can only show stranding the advisories record.

The fair view

Maintaining more than one release line costs time that most open source maintainers don't have. Many of the packages in this data have one or two maintainers, and asking them for LTS branches is asking for unpaid work. Sometimes a fix can't be backported without the change that makes it a fix. And many consumers on old lines are there because they haven't upgraded, not because they can't.

But the cost of not knowing falls on the consumer, and it's avoidable. A one-line support policy and an accurate affected range cost a maintainer little and save every user the guesswork. For the rest, it's on consumers to stop reading "fixed in a newer major" as "fixed". Safeguard's free Gold directory lets you look up a package's known vulnerabilities by name, which is a quick way to check an older line.

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.