When a vulnerability has no patch, GitHub's advisory database usually records it as "affects everything up to and including version X", where X is the newest version anyone confirmed as vulnerable. Any release above X, including ones that already existed, is treated as unaffected by every scanner that reads the database, whether or not it fixed anything. We measured how often that has already happened to 2026 advisories.
This post is for teams that rely on dependency scanners for npm, PyPI and Go, and for anyone who reads "no known vulnerabilities" as a clean bill of health. It covers what the data shows, why the ranges are written this way, and a script that lists the dependencies in your lockfiles that pass a scan only because they're newer than a "no fix" ceiling.
The short answer
Of 7,529 GitHub-reviewed advisories published for Go, npm and PyPI packages in 2026 up to 18 October, 569 (7.6%) record no fixed version, and 534 of those use the newest known affected version as a ceiling instead. Scanners treat every version above that ceiling as unaffected. For 234 of the 430 we could check (54%), the package already has at least one stable release above the ceiling, so those versions scan clean whether or not they contain a fix. For 98 of 173 npm and PyPI package ceilings, that release was out before the advisory was even published. If an advisory on one of your dependencies lists no patched version, confirm the fix yourself before you trust a clean result.
How "no fix" gets written down
An advisory needs a version range so tools can match it. When a fix exists, the range ends at the fixed version: everything before it is affected, the fix and everything after it are not. When no fix exists yet, there is nothing to end the range at, so reviewers have two choices:
| Range | What it says | What scanners do with a newer release |
|---|---|---|
introduced: 0, last_affected: X (shown on GitHub as <= X) | Affected up to and including X | Report it clean |
introduced: 0 and nothing else | Every version is affected | Keep flagging it |
The OSV schema, which GitHub's data is published in, describes last_affected as "the hard ceiling of the vulnerability at the time of publication" and says versions above it "should be considered unaffected". It also names the cost: this "opens up the possibility for false negatives", which is why the schema strongly prefers a fixed event whenever one is known. For a bug with no fix, there isn't one.
GitHub's own advisory pages show the same range as affected versions <= X and patched versions "none". npm audit, OSV-Scanner and other tools built on this data all compare your installed version against that ceiling.
What the data shows
We took every GitHub-reviewed advisory for Go, npm and PyPI in OSV's data dumps on 18 October 2026, dropped withdrawn records and malware, and looked for advisories where no affected package has a fixed version.
GitHub-reviewed advisories with no fixed version, by year published
(counted once per ecosystem: 2026 has 7,557 entries for 7,529 advisories)
2024 286 of 1,717 (16.7%)
2025 320 of 1,988 (16.1%)
2026 570 of 7,557 (7.5%)
2026 by ecosystem 2026 by severity
Go 184 of 2,117 (8.7%) critical 103 of 875 (11.8%)
PyPI 219 of 2,247 (9.7%) high 196 of 3,072 (6.4%)
npm 167 of 3,193 (5.2%) moderate 191 of 3,027 (6.3%)
low 80 of 583 (13.7%)
2026 no-fix advisory entries
using a last_affected ceiling 534 of 570
open-ended (every version) 36 of 570
older than 90 days 503 of 570
rated critical or high 299 of 570
Three things stand out. First, nearly all 2026 no-fix advisories, 534 of 569, use a ceiling rather than an open-ended range. Second, "no fix" isn't a low-severity problem: critical advisories lack a fix more often than high or moderate ones. Third, records don't seem to gain a fix with age. About one in six advisories from 2024 and 2025 still record none, so a ceiling written on day one tends to stay the ceiling. The 2026 share is lower partly because recent months are dominated by advisories published with a fix already available: August to October run at 2% to 4%, against 14% in May.
How many ceilings have already been passed
For each 2026 advisory with a last_affected ceiling and no fix, we asked the package's registry (npm, PyPI or the Go module proxy) whether anything newer than the ceiling has been released since.
| Result | |
|---|---|
| 2026 no-fix advisories with a ceiling | 534 |
| Checkable (registry answered, ceiling version found) | 430 |
| Ceiling passed by at least one stable release | 234 (54.4%) |
| By ecosystem | Go 68 of 97 (70%), npm 70 of 139 (50%), PyPI 96 of 194 (49%) |
| Of those 234, by severity | critical 36, high 83, moderate 78, low 37 |
| Distinct packages | 143 |
| Stable releases above the ceiling, per advisory | median 7 |
| npm and PyPI package ceilings: first stable release above the ceiling | before the advisory in 98 of 173; otherwise a median of 15 days after |
So for more than half of the checkable advisories, a team that keeps its dependencies current is already on a version no scanner will flag. That's usually good news, because maintainers often fix the bug in the next release. But the data can't tell you which case you're in. We read ten of the advisories whose ceiling was passed before publication. One said in its text which release fixes the bug, and it was the first release above the ceiling. The other nine didn't say whether later versions are fixed.
The 98 cases where the newer release came first are the most surprising. The ceiling there isn't "the latest version on publication day". It is most likely the version the reporter tested, while newer versions already existed. Those newer versions were never assessed, and they have scanned clean from day one.
We couldn't check 104 advisories, mostly Go modules whose ceiling isn't a tagged version in the module proxy's list. Go's higher rate above should be read with that in mind.
Find the dependencies that pass on a ceiling
This script reads package-lock.json, pinned requirements.txt files and go.mod, asks OSV for every advisory on each package, and prints the ones where your version is above a last_affected ceiling with no fix recorded. It uses only the Python standard library:
#!/usr/bin/env python3
"""List dependencies that pass a scan only because they are newer than an
advisory's last_affected ceiling (no fixed version recorded).
Usage: python3 ceiling_check.py package-lock.json | requirements.txt | go.mod ...
Reads pinned versions, asks OSV for every advisory on each package, and prints
the ones where your version sits above last_affected with no fix event.
"""
import json, re, sys, urllib.request
OSV = "https://api.osv.dev/v1/query"
def parse(path):
if path.endswith("package-lock.json"):
lock = json.load(open(path))
for key, meta in lock.get("packages", {}).items():
if key and "node_modules/" in key and meta.get("version"):
yield "npm", key.rsplit("node_modules/", 1)[1], meta["version"]
elif path.endswith("go.mod"):
for m in re.finditer(r"^\s*(?:require\s+)?([\w.\-/]+\.[\w.\-/]+)\s+(v[\w.\-+]+)", open(path).read(), re.M):
yield "Go", m.group(1), m.group(2)
else:
for line in open(path):
m = re.match(r"\s*([A-Za-z0-9._\-]+)(?:\[[^\]]*\])?\s*==\s*([\w.!+\-]+)", line)
if m:
yield "PyPI", m.group(1), m.group(2)
def key(v):
"""Rough ordering for semver, PEP 440 and Go versions; good enough for a triage list."""
v = v.lstrip("v").split("+")[0]
m = re.match(r"(\d+(?:\.\d+)*)(.*)", v)
if not m:
return ((0,), 0, v)
nums = tuple(int(x) for x in m.group(1).split("."))
nums += (0,) * (4 - len(nums))
rest = m.group(2).lstrip(".-")
return (nums, 0 if rest and not rest.startswith("post") else 1, rest)
def advisories(eco, name):
body = json.dumps({"package": {"name": name, "ecosystem": eco}}).encode()
req = urllib.request.Request(OSV, body, {"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r).get("vulns", [])
def main(paths):
seen, hits = set(), 0
for path in paths:
for eco, name, ver in parse(path):
if (eco, name, ver) in seen:
continue
seen.add((eco, name, ver))
shown = set()
for v in advisories(eco, name):
ids = {v["id"], *v.get("aliases", [])}
if ids & shown:
continue
for a in v.get("affected", []):
if a["package"]["name"] != name:
continue
events = [e for rg in a.get("ranges", []) for e in rg.get("events", [])]
if any("fixed" in e for e in events):
continue
ceilings = [e["last_affected"] for e in events if "last_affected" in e]
if ceilings and all(key(ver) > key(c) for c in ceilings):
hits += 1
shown |= ids
print(f"{eco:5} {name} {ver}: {v['id']} last_affected {max(ceilings, key=key)}, "
f"no fix recorded. Check the advisory before treating {ver} as clean.")
print(f"\n{len(seen)} packages checked, {hits} above a no-fix ceiling")
if __name__ == "__main__":
main(sys.argv[1:])
On a small test project with three npm packages and three PyPI packages, each pinned to a current release. We've replaced the two flagged package names and their advisory IDs, because the point is the pattern, not the projects:
$ python3 ceiling_check.py package-lock.json requirements.txt
npm npm-package-a 10.26.0: GHSA-xxxx-xxxx-xx01 last_affected 10.25.7, no fix recorded. Check the advisory before treating 10.26.0 as clean.
npm npm-package-a 10.26.0: GHSA-xxxx-xxxx-xx02 last_affected 10.24.0, no fix recorded. Check the advisory before treating 10.26.0 as clean.
npm npm-package-a 10.26.0: GHSA-xxxx-xxxx-xx03 last_affected 10.25.7, no fix recorded. Check the advisory before treating 10.26.0 as clean.
npm npm-package-a 10.26.0: GHSA-xxxx-xxxx-xx04 last_affected 10.24.0, no fix recorded. Check the advisory before treating 10.26.0 as clean.
npm npm-package-a 10.26.0: GHSA-xxxx-xxxx-xx05 last_affected 10.25.7, no fix recorded. Check the advisory before treating 10.26.0 as clean.
npm npm-package-a 10.26.0: GHSA-xxxx-xxxx-xx06 last_affected 10.25.7, no fix recorded. Check the advisory before treating 10.26.0 as clean.
PyPI pypi-package-b 1.0.1: GHSA-xxxx-xxxx-xx07 last_affected 1.0.0, no fix recorded. Check the advisory before treating 1.0.1 as clean.
PyPI pypi-package-b 1.0.1: GHSA-xxxx-xxxx-xx08 last_affected 1.0.0, no fix recorded. Check the advisory before treating 1.0.1 as clean.
PyPI pypi-package-b 1.0.1: GHSA-xxxx-xxxx-xx09 last_affected 1.0.0, no fix recorded. Check the advisory before treating 1.0.1 as clean.
6 packages checked, 9 above a no-fix ceiling
At these versions, none of the nine flagged advisories comes back from OSV's API, and npm's audit endpoint doesn't return the six npm ones either. Both packages do have other advisories that tools still report, so a scan isn't silent. It just leaves these out. Each line is a question, not a verdict. Open the advisory, then the project's changelog or the linked issue, and decide whether your version contains the fix. Its version comparison is deliberately simple, so treat unusual version strings as a prompt to look, not as proof.
When the answer is "not fixed" or "can't tell", the useful next question is whether your code reaches the vulnerable function at all. Here's Safeguard's interactive walkthrough of an exploitability check:
What to do
- Run the check on your lockfiles and read every advisory it lists. Most will turn out to be fixed. The ones that aren't are invisible to your scanner.
- Record what you find. If a newer version did fix it, say so in your tracker or a VEX statement. If it didn't, keep a finding open by hand, because no tool will raise it again.
- Ask maintainers to update the advisory when you can confirm a fixed version. A
fixedevent helps everyone downstream, and GitHub's advisory database accepts community contributions to existing advisories. - Watch "no fix" advisories on packages you depend on, not just your scanner's output. Subscribing to the repository's security advisories is enough for the few that matter.
- Plan for the unfixable ones. Our post on unpatchable vulnerabilities covers compensating controls, risk acceptance and pushing upstream.
The fair view
The ceiling is a reasonable default. A maintainer who ships a release after a report often includes the fix, and a range that flagged every future version forever would bury teams in alerts for bugs that were fixed long ago. The schema authors chose the trade-off openly and documented the cost. Reviewers can only record a fix once someone tells them about it.
The gap is in what happens next. Nothing in the pipeline confirms that the release after the ceiling actually fixed the bug, so a clean result for that version is an assumption, not a finding. For most dependencies the assumption is right. For the ones that sit behind your login page or parse untrusted input, it's worth the few minutes to check.
Check yourself
An advisory lists versions up to and including 2.3.0 as affected, with no patched version. You run 2.3.1. What does your scanner report?
Nothing. Versions above a last_affected ceiling are treated as unaffected, whether or not 2.3.1 fixed the bug. Read the advisory and the changelog to find out.
Why don't databases just mark every version as affected until a fix is confirmed?
Because most releases after an advisory do contain the fix, and an open-ended range would keep flagging them until someone updated the record. That trades false negatives for a lot of false positives. The OSV schema allows both and prefers a fixed version whenever one is known.
You confirm that version 2.3.1 fixed the bug. What's the most useful thing to do?
Record it in your own tracker, then suggest the fixed version to the advisory database so the range ends in a fixed event. That turns an assumption into a fact for everyone who uses the package.