fast-uri, the URI parser that ajv and Fastify depend on, fixed four vulnerabilities on 19 October 2026 (UTC). The scores are 6.1, 4.8, 3.7 and 3.7. Update every copy to 2.4.8, 3.1.9 or 4.2.3, depending on its major line, and check for nested copies your lockfile still pins.
This post is for JavaScript and TypeScript teams, which in practice means most of them, because fast-uri usually arrives as a dependency of a dependency. It covers what each of the four bugs does, the exact affected and fixed versions, the code change behind each fix, how fast-uri reaches your tree through ajv and Fastify, how to find and update every copy with npm, Yarn and pnpm, and a small self test you can run against whatever version you have installed.
The short answer
Update fast-uri to 2.4.8, 3.1.9 or 4.2.3 on whichever major line you use. The four CVEs are CVE-2026-110306 (a percent-encoded scheme decoded into a live scheme, CVSS 6.1), CVE-2026-110180 (host confusion from a second parser's canonicalization, 4.8), CVE-2026-110725 (scheme injection with the non-default suffix reference option, 3.7) and CVE-2026-110095 (dot-segment removal applied to relative references, 3.7). They matter when your code, or a library you use, parses untrusted URIs with fast-uri and makes a security decision on one form of the URI while using another. ajv, the most common route fast-uri takes into a project, uses it to resolve schema $id and $ref values, which usually come from schemas you wrote yourself.
Three facts are worth knowing before you start:
- A fresh install already picks up the fixes, but a lockfile written before 19 October keeps the old copy until someone updates it.
npm installrespects the lockfile. - A Fastify 5 application usually carries two copies of fast-uri: a 3.x copy for ajv and a 4.x copy for
@fastify/ajv-compilerandfast-json-stringify. Both need the fix, and a single global override can break one of them. - As of 23 October, all four advisories are in GitHub's reviewed advisory database and in OSV, so
npm auditand most scanners now report them. On 20 October they did not.
What changed since 20 October
We first wrote about these fixes on 20 October, the morning the CVE records appeared. Since then:
- GitHub reviewed all four advisories and added them to the GitHub Advisory Database on 23 October, between 21:36 and 21:41 UTC, and OSV imported them in the same minutes. On the morning of 20 October, when we checked, neither database had them. On 25 October,
npm audit --package-lock-onlyon a lockfile pinned to 3.1.8 listed all four. - CISA added an assessment to each CVE record on 20 October: exploitation "none", automatable "no", technical impact "partial". None of the four is in the Known Exploited Vulnerabilities catalog as of its 23 October release.
- NVD lists all four as "Awaiting Analysis", so the only scores are the ones the OpenJS Foundation CNA assigned.
- No newer fast-uri release has appeared. 4.2.3, 3.1.9 and 2.4.8 are still the newest on their lines.
- The credits are clearer. The CVE records credit zekth as the reporter of CVE-2026-110306, CVE-2026-110180 and CVE-2026-110095, and Ulises Gascon (UlisesGascon) as the finder of CVE-2026-110725. The GitHub advisories list only reviewers for the first three.
- Downloads went up. npm's downloads API reports 98,713,361 downloads of fast-uri for 16 to 22 October 2026, against about 91 million for 12 to 18 October.
Timeline
All times are UTC. Commit times are author dates from the git history, which show when a change was written, not when it became public.
| Date and time (UTC) | Event | Source |
|---|---|---|
| 15 Sep 2026 | Earlier fixes 4.1.5, 3.1.8 and 2.4.7 ship; CVE-2026-86472 and CVE-2026-86818 published | npm registry, CVE records |
| 10 Oct 2026, 18:28 | Fix commits for the scheme, relative reference and domain host issues authored | fast-uri git history |
| 13 Oct 2026, 16:04 to 19:49 | CVE-2026-110095, CVE-2026-110180 and CVE-2026-110306 reserved | CVE records |
| 14 Oct 2026, 12:05 to 14:53 | Suffix colon fix authored; CVE-2026-110725 reserved | git history, CVE record |
| 18 Oct 2026, 10:31 | Release workflows updated; 2.x and 3.x lines move to trusted publishing | fast-uri git history |
| 19 Oct 2026, 11:00 to 14:02 | Follow-up fixes keep relative paths relative; a 2.x-only fix keeps suffix authority parsing unchanged | fast-uri git history |
| 19 Oct 2026, 22:51:36 | fast-uri 4.2.3 published | npm registry |
| 19 Oct 2026, 23:05:26 | fast-uri 3.1.9 published | npm registry |
| 19 Oct 2026, 23:10:34 | fast-uri 2.4.8 published | npm registry |
| 19 Oct 2026, 23:35:07 to 23:45:38 | The four GitHub repository advisories published | GitHub |
| 20 Oct 2026, 06:56:56 to 07:06:29 | The four CVE records published | CVE records |
| 20 Oct 2026, 07:17:08 | All four received by NVD | NVD |
| 20 Oct 2026, 16:06 to 19:14 | CISA ADP adds its assessment to each record | CVE records |
| 23 Oct 2026, 21:36:40 to 21:41:36 | GitHub reviews all four into the Advisory Database; OSV imports them | GitHub, OSV |
| 25 Oct 2026 | npm audit reports all four against a lockfile pinned to 3.1.8 (our check) | npm audit |
The fixed releases on all three lines shipped within 19 minutes of each other, and the advisories followed within an hour. It then took about four days for the advisories to reach the shared databases most tools read. That delay is normal for a repository advisory awaiting review, and it is the window in which a scanner reading only those databases said nothing.
How it works, in plain language
A URI parser splits a string like https://example.com/a/b?q=1 into parts: scheme, host, path, query. Applications then decide things based on those parts. Is this link safe to render? Is this host allowed? Does this reference stay inside a folder?
Each of the four bugs is a disagreement, either between fast-uri and RFC 3986 or between fast-uri and the WHATWG URL parser built into Node.js and browsers. A disagreement becomes a security problem when an application checks one form of a URI and then acts on another.
- CVE-2026-110306, scheme decoding. RFC 3986 does not allow percent-encoding in a scheme. fast-uri decoded the scheme with JavaScript's legacy
unescape()before validating it, so a scheme with an encoded letter came out ofnormalize()orresolve()as a real scheme. An app that screenedjavascript:on the raw input and then stored or rendered fast-uri's output could end up with the scheme it meant to block, andequal()treated the encoded and plain forms as the same. - CVE-2026-110180, host canonicalization. For HTTP-style schemes, fast-uri passed the host to the WHATWG parser and adopted its hostname without checking that the whole input had been read as a host. The WHATWG parser treats
@as the end of a username and strips tab, carriage return and line feed, so data before an@was dropped and whitespace disappeared, with no error. - CVE-2026-110725, the suffix option. With the non-default
reference: "suffix"option, serializing a reference with no authority could leave a bare colon in the first path segment, so the output reads as an absolute URI with a scheme. - CVE-2026-110095, dot segments. RFC 3986 removes
.and..only from the merged path produced during resolution. fast-uri also did it to relative references on their own, so a leading../could be dropped anda/../bbecame/b, pointing at the site root instead of the current folder.
Technical details
The CVE records
The OpenJS Foundation is the CVE Numbering Authority for all four. The records were reserved on 13 and 14 October and published on 20 October. Every record lists pkg:npm/fast-uri as the affected package with semver ranges and references both the matching GitHub advisory and the OpenJS CNA advisories page.
| CVE | GHSA | Title in the record | CVSS 3.1 | Severity |
|---|---|---|---|---|
| CVE-2026-110306 | GHSA-w74x-v6f9-j3xg | URI scheme spoofing via percent-encoded scheme decoding | 6.1 | Medium (GitHub: Moderate) |
| CVE-2026-110180 | GHSA-wjr9-j795-jxcr | Host confusion via unchecked WHATWG host canonicalization | 4.8 | Medium (GitHub: Moderate) |
| CVE-2026-110725 | GHSA-333x-w4r6-6crp | Scheme injection via an unescaped first-segment colon in suffix references | 3.7 | Low |
| CVE-2026-110095 | GHSA-xhq7-xmqj-769h | Improper path equivalence in relative reference normalization | 3.7 | Low |
Vectors, weaknesses and enrichment
| CVE | CVSS 3.1 vector | CWE | EPSS on GitHub, 25 Oct |
|---|---|---|---|
| CVE-2026-110306 | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N | CWE-172 Encoding Error; CWE-180 Validate Before Canonicalize | 0.204% |
| CVE-2026-110180 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N | CWE-20 Improper Input Validation; CWE-436 Interpretation Conflict | 0.208% |
| CVE-2026-110725 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N | CWE-41 Improper Resolution of Path Equivalence; CWE-116 Improper Encoding or Escaping of Output | 0.254% |
| CVE-2026-110095 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N | CWE-41 Improper Resolution of Path Equivalence | 0.232% |
CVE-2026-110306 is the only one with low attack complexity, and it needs user interaction with a scope change, the usual shape of a cross-site scripting score: the flaw is in a library, the harm lands in whatever renders its output. The other three are high complexity because they depend on how an application uses the parser. EPSS figures change daily; all four sat between the 10th and 16th percentiles.
Credits
| CVE | CVE record credits | GitHub advisory credits |
|---|---|---|
| CVE-2026-110306 | zekth (reporter); UlisesGascon, mcollina (remediation reviewers) | UlisesGascon, mcollina (remediation reviewers) |
| CVE-2026-110180 | zekth (reporter); UlisesGascon, mcollina (remediation reviewers) | UlisesGascon, mcollina (remediation reviewers) |
| CVE-2026-110725 | UlisesGascon (finder); mcollina (remediation reviewer) | UlisesGascon (finder); mcollina (remediation reviewer) |
| CVE-2026-110095 | zekth (reporter); UlisesGascon, mcollina (remediation reviewers) | UlisesGascon, mcollina (remediation reviewers) |
zekth is also listed among fast-uri's npm maintainers. GitHub labels GHSA-333x-w4r6-6crp as "submitted with AI assistance".
The fix for CVE-2026-110306: stop decoding schemes
Before the fix, a helper called decodeValidScheme ran the scheme through unescape() and then tested the decoded result. The 4.2.3 diff renames it validateScheme and drops the decoding: the scheme is tested as written against ^[A-Za-z][A-Za-z0-9+.-]*$, RFC 3986's scheme rule, so a scheme containing % fails. The parser gets the same change.
Per the updated README, a percent-encoded scheme now gets error: 'URI scheme is malformed.', normalize() returns the input unchanged, equal() returns false, and resolve() and serialize() throw. On the published tarballs, 3.1.9 keeps a scheme written %68ttp as a literal, malformed scheme, while 3.1.8's equal() treats it as plain http.
The fix for CVE-2026-110180: reject what the second parser would rewrite
The fix adds two checks around canonicalizeHost().
First, a new hasForbiddenDomainHost() rejects a domain host containing any ASCII character outside RFC 3986's reg-name set: control characters, space, DEL, @ and the other delimiters. It applies to http, https, ws, wss and any scheme with the domainHost option, skips IP literals, and still allows non-ASCII characters for internationalized names. A rejected host gets error: 'URI host is malformed.' and never reaches the WHATWG parser.
Second, a new isHostOnlyURL() checks that the WHATWG parser read the whole input as a host: no username, password, port, query or fragment, a path of / and a non-empty hostname. Anything else means the second parser found a delimiter fast-uri did not, and the host is marked malformed. resolve() runs the same check and throws. In our test, 3.1.8 parsed an HTTP URI with two @ characters into a host of only the last part, while 3.1.9 kept the full string and set the error.
The fix for CVE-2026-110095: a separate routine for relative paths
The fix adds removeDotSegmentsRelative() in lib/utils.js. For a path not starting with /, a .. only cancels a real segment before it; a leading .. is kept, the result never becomes root-relative, and a result that would be empty or start with // gets a . prefix. normalize(), serialize() and the merge step in resolve() use it for schemeless references without an authority. The README's examples: a/../b becomes b, ../x stays ../x, x/.. becomes ./, and a/../a:b becomes ./a:b. On 3.1.8, a/../x:y normalized to /x:y, root-relative with a colon in its first segment.
The fix for CVE-2026-110725: escape the colon in suffix output too
In serialize(), the colon in a first path segment was escaped only when there was no scheme and no authority. With the suffix option the scheme is set but not written out, so the colon slipped through. A new schemelessReference flag is also true for reference: "suffix", and the escaping now runs whenever it is set, including after dot-segment removal. The README adds a warning that suffix references are ambiguous by design and that suffix output should be validated with parse() before it is rendered or checked against a scheme allowlist. Each release adds four security test files, and the 2.x line has one extra commit to keep its older suffix authority parsing.
Registry details for the fixed releases
All three fixed versions were published from GitHub Actions through npm trusted publishing with SLSA provenance attestations; 3.1.8 and 2.4.7 had been published from a maintainer account without one. Verify what you installed against these values:
| Version | Published (UTC) | dist.integrity | dist.shasum |
|---|---|---|---|
| 4.2.3 | 19 Oct 22:51:36 | sha512-I1PXwP5eAhlNlcdpjt9CgB59op2ZvZfxw+BkqGFVIUOgNpCVTcTqp3HhTX7brxj5vsip+nSY4nTvff+eIA76hg== | 4f0f696864eab654e1bd0aa2557bd68f61b3a0e0 |
| 3.1.9 | 19 Oct 23:05:26 | sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ== | f1deba822b1d6ac79da2f860e872a63470cff203 |
| 2.4.8 | 19 Oct 23:10:34 | sha512-knw8ym69bDOF980l2z0tDKh7XB9zfsUmpMmoQUx9KskazmddvnhNntiTzen4VsiW7qVWiVZkrcd5Pj2bziC3gA== | 7283e40a4ee32deccf04610dcf0d47f88abe2905 |
The 3.1.8 and 3.1.9 tarballs we downloaded matched the registry's shasum values.
Affected and fixed versions
| CVE | 2.x affected | 3.x affected | 4.x affected | Fixed in |
|---|---|---|---|---|
| CVE-2026-110306 | all before 2.4.8 | 3.0.0 through 3.1.8 | 4.0.0 through 4.2.2 | 2.4.8, 3.1.9, 4.2.3 |
| CVE-2026-110180 | 2.4.2 through 2.4.7 | 3.1.3 through 3.1.8 | 4.0.1 through 4.2.2 | 2.4.8, 3.1.9, 4.2.3 |
| CVE-2026-110725 | all before 2.4.8 | 3.0.0 through 3.1.8 | 4.0.0 through 4.2.2 | 2.4.8, 3.1.9, 4.2.3 |
| CVE-2026-110095 | all before 2.4.8 | 3.0.0 through 3.1.8 | 4.0.0 through 4.2.2 | 2.4.8, 3.1.9, 4.2.3 |
| CVE-2026-86472 (Sep) | all before 2.4.7 | 3.0.0 through 3.1.7 | 4.0.0 through 4.1.4 | 2.4.7, 3.1.8, 4.1.5 |
| CVE-2026-86818 (Sep) | not affected | not affected | 4.1.3 and 4.1.4 | 4.1.5 |
The CVE-2026-110180 record starts its ranges at 2.4.2, 3.1.3 and 4.0.1. Older copies fall outside that one but are still affected by the other three. The September issues are listed because anyone on a version older than 3.1.8 or 4.1.5 also misses those fixes. Updating to the current fixed versions covers all six.
The fixed versions in plain terms: on the 2.x line use 2.4.8 or later, on 3.x use 3.1.9 or later, and on 4.x use 4.2.3 or later. There is no 1.x fix; a 1.x copy should move to a supported line.
Who is affected
You are exposed to the outcomes the records describe if an affected copy runs in your application, untrusted input reaches its parsing functions, and your code decides on one form of the URI while using another. Most projects get fast-uri through other packages, so the question is which ones and what they feed it.
How fast-uri gets into your tree
We read the dependency ranges for every published version of the main packages from the npm registry.
| Package | Versions | fast-uri range | Line you get |
|---|---|---|---|
| ajv | 8.17.1 through 8.20.0 | ^3.0.1 | 3.x |
| ajv | 8.15.0 | ^2.3.0 | 2.x |
| ajv | 6.x, 7.x, 8.0.0 through 8.14.0, 8.16.0 | none (no fast-uri dependency) | not affected by these CVEs |
| @fastify/ajv-compiler | 4.0.6, 4.0.7 | ^4.0.0 | 4.x |
| @fastify/ajv-compiler | 4.0.0 through 4.0.5 | ^3.0.0 | 3.x |
| @fastify/ajv-compiler | 3.1.1 through 3.6.0 | ^2.0.0 | 2.x |
| fast-json-stringify | 7.0.1 through 7.2.0 | ^4.0.0 | 4.x |
| fast-json-stringify | 6.0.1 through 7.0.0 | ^3.0.0 | 3.x |
| fast-json-stringify | 5.x | ^2.1.0 | 2.x |
| fastify | 5.7.0 through 5.12.7 | via @fastify/ajv-compiler ^4.0.5 and fast-json-stringify ^7.0.0 | 4.x, plus 3.x through ajv |
| fastify | 4.x | via @fastify/ajv-compiler ^3.5.0 and fast-json-stringify ^5.8.0 | 2.x, plus 3.x through ajv |
So a Fastify 5 application normally has 4.x from Fastify's packages and 3.x from ajv, and a Fastify 4 application has 2.x and 3.x. Many build tools and linters depend on ajv, so fast-uri also turns up in development dependencies and in tools baked into images.
What these packages do with fast-uri
ajv's default URI resolver is fast-uri: lib/runtime/uri.ts imports it, and the UriResolver interface ajv expects has three methods, parse, resolve and serialize. ajv uses them to build full schema IDs and to resolve $ref values against the current base ID while compiling a schema. @fastify/ajv-compiler passes fast-uri explicitly as uriResolver in its default ajv options, and fast-json-stringify does the same in lib/validator.js.
That is a narrow use. The inputs are schema identifiers you wrote, the output feeds schema lookup rather than a browser or an outbound request, and ajv does not call normalize() or equal() through this interface. We have not seen a published analysis showing the records' outcomes through ajv's own use. The case to think about is a service that compiles schemas supplied by its users; even there, the realistic result is a reference resolving to a different schema than intended.
Where direct use is more likely to matter
Direct use for links, redirects or outbound requests is where these bugs matter. Look for:
- Link sanitizers or rich text pipelines that accept a URL, check its scheme, and store a normalized form.
- Redirect validators that compare a parsed host against an allowlist.
- Server-side request code that blocks loopback or internal hosts based on a parsed host, then hands the original string to an HTTP client.
- Caches, deduplication tables or authorization checks keyed on a normalized relative reference.
- Any use of the
reference: "suffix"option on input you did not write.
What to do now
Step 1: list every copy
With npm, list the whole tree and explain each path:
npm ls fast-uri --all
npm explain fast-uri
npm audit --package-lock-only
Since 23 October, npm audit lists the four advisories against any affected copy. With Yarn and pnpm:
# Yarn 2 and later
yarn why fast-uri
yarn npm audit --recursive
# Yarn 1
yarn why fast-uri
yarn audit
# pnpm
pnpm why fast-uri
pnpm audit
For a container image, find the installed copies directly:
docker run --rm --entrypoint sh your-image:tag -c \
'find / -path "*/node_modules/fast-uri/package.json" 2>/dev/null | while read f; do printf "%s " "$f"; grep -m1 "\"version\"" "$f"; done'
Step 2: check lockfiles across many repositories
Across many repositories, reading lockfiles is faster than installing each one. This standard library script reads npm (lockfile versions 1 to 3), Yarn and pnpm lockfiles, lists every fast-uri version and exits with status 1 if any copy needs the 19 October fixes, so it can run in CI.
#!/usr/bin/env python3
"""List every fast-uri in npm, Yarn and pnpm lockfiles and flag copies that
miss the 19 October 2026 fixes (CVE-2026-110306, CVE-2026-110180,
CVE-2026-110725, CVE-2026-110095).
Fixed: 2.4.8 on 2.x, 3.1.9 on 3.x, 4.2.3 on 4.x. Standard library only.
Usage: python3 fast_uri_lock_check.py LOCKFILE [LOCKFILE ...]
"""
import json
import re
import sys
FIXED = {2: (2, 4, 8), 3: (3, 1, 9), 4: (4, 2, 3)}
def vtuple(v):
core = re.match(r"(\d+)\.(\d+)\.(\d+)", v)
return tuple(int(x) for x in core.groups()) if core else None
def status(v):
t = vtuple(v)
if t is None:
return "CHECK unparsed version"
fixed = FIXED.get(t[0])
if fixed is None:
return "CHECK no fixed release on this major line"
if t < fixed:
return "UPDATE to " + ".".join(map(str, fixed))
return "ok"
def from_npm(text):
lock = json.loads(text)
rows = []
for path, meta in lock.get("packages", {}).items():
if path.endswith("node_modules/fast-uri"):
parent = path[: -len("node_modules/fast-uri")].rstrip("/")
where = parent.split("node_modules/")[-1] if parent else "(top level)"
rows.append((meta.get("version", "?"), where))
if not rows: # lockfileVersion 1 nests a dependencies tree
def walk(deps, owner):
for name, meta in (deps or {}).items():
if name == "fast-uri":
rows.append((meta.get("version", "?"), owner))
walk(meta.get("dependencies"), name)
walk(lock.get("dependencies"), "(top level)")
return rows
def from_text(text):
# yarn.lock entries: fast-uri@^3.0.1: then version "3.1.8"
# pnpm-lock.yaml keys: /fast-uri@3.1.8: or fast-uri@3.1.8:
rows = []
for m in re.finditer(r'^"?fast-uri@[^\n]*:\n\s+version:? "?([0-9][^"\s]*)', text, re.M):
rows.append((m.group(1), "(yarn entry)"))
for m in re.finditer(r"^\s+/?fast-uri@(\d+\.\d+\.\d+[^:\s(]*)", text, re.M):
rows.append((m.group(1), "(pnpm entry)"))
return rows
def main(paths):
worst = 0
for path in paths:
text = open(path, encoding="utf-8").read()
rows = from_npm(text) if path.endswith(".json") else from_text(text)
print(f"== {path}")
if not rows:
print(" no fast-uri")
for v, where in sorted(set(rows)):
s = status(v)
worst = max(worst, 0 if s == "ok" else 1)
print(f" fast-uri {v:<8} {where:<28} {s}")
return worst
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
We ran it against five made-up projects: a Fastify 5 API whose lockfile predates the fix, a web app that has already updated, an old tool with a version 1 lockfile, a Yarn project and a pnpm project.
$ python3 fast_uri_lock_check.py api/package-lock.json web/package-lock.json \
tool/package-lock.json yarnapp/yarn.lock pnpmapp/pnpm-lock.yaml
== api/package-lock.json
fast-uri 3.1.8 (top level) UPDATE to 3.1.9
fast-uri 4.2.2 @fastify/ajv-compiler UPDATE to 4.2.3
== web/package-lock.json
fast-uri 3.1.9 ajv ok
fast-uri 4.2.3 (top level) ok
== tool/package-lock.json
fast-uri 2.4.7 legacy-schema UPDATE to 2.4.8
== yarnapp/yarn.lock
fast-uri 3.1.6 (yarn entry) UPDATE to 3.1.9
== pnpmapp/pnpm-lock.yaml
fast-uri 3.1.9 (pnpm entry) ok
fast-uri 4.2.1 (pnpm entry) UPDATE to 4.2.3
$ echo $?
1
"(top level)" means the hoisted copy in the root node_modules, not necessarily a dependency you listed. A named owner means a nested copy. For Yarn and pnpm, use yarn why or pnpm why to find the parent.
Step 3: update within the existing ranges
Every package in the table above uses a caret range that the fixed versions satisfy, so refreshing the lockfile is usually enough:
# npm
npm update fast-uri
npm ls fast-uri --all
# Yarn 2 and later: re-resolve every fast-uri in the tree
yarn up -R fast-uri
# pnpm
pnpm update fast-uri
On Yarn 1, yarn upgrade does not reliably move transitive copies. Remove the fast-uri@ entries from yarn.lock and run yarn install to re-resolve them, then check yarn.lock again.
Commit the lockfile, then rebuild and redeploy every image and bundle that copied node_modules at build time.
Step 4: force nested copies only if you must
If a package pins an old range, for example a tool that still depends on fast-uri 2.4.7 exactly, the update above will not move it. Update that package first. If there is no newer version, use a scoped override so each line gets its own fixed version:
{
"overrides": {
"ajv": { "fast-uri": "^3.1.9" },
"@fastify/ajv-compiler": { "fast-uri": "^4.2.3" },
"fast-json-stringify": { "fast-uri": "^4.2.3" }
}
}
The equivalent in pnpm goes under pnpm.overrides in package.json with the parent>child form, for example "ajv>fast-uri": "^3.1.9". In Yarn, use resolutions with the same scoping, for example "ajv/fast-uri": "^3.1.9". Avoid a single unscoped override such as "fast-uri": "^3.1.9": it forces Fastify's 4.x consumers onto 3.x, which is outside the range they declare.
Step 5: verify what you installed
Check the version and integrity of each installed copy against the registry values in the table above, and verify the provenance attestations:
npm ls fast-uri --all
npm view fast-uri@3.1.9 dist.integrity
npm audit signatures
Step 6: run a behavior self test
A version check tells you what a package claims to be. This Node.js script checks how the copy you load behaves on the inputs the advisories describe, using harmless example hosts:
// Usage: node fast-uri-selftest.js [path-to-fast-uri]
const uri = require(process.argv[2] || 'fast-uri')
const v = require((process.argv[2] || 'fast-uri') + '/package.json').version
const checks = [
['CVE-2026-110306 encoded scheme is not decoded',
() => uri.equal('%68ttp://a.example/', 'http://a.example/') === false],
['CVE-2026-110180 second @ in host is an error',
() => Boolean(uri.parse('http://a@b@c.example/').error)],
['CVE-2026-110095 leading .. kept on relative normalize',
() => uri.normalize('../x') === '../x'],
['CVE-2026-110095 relative path stays relative',
() => uri.normalize('a/../b') === 'b'],
]
console.log('fast-uri', v)
for (const [name, fn] of checks) {
let ok
try { ok = fn() } catch (e) { ok = 'threw: ' + e.message }
console.log((ok === true ? 'PASS ' : 'FAIL ') + name + (ok === true || ok === false ? '' : ' (' + ok + ')'))
}
We ran it on Node.js 26.7.0 against the published 3.1.8 and 3.1.9 tarballs:
$ node fast-uri-selftest.js ./pkg-3.1.8/package
fast-uri 3.1.8
FAIL CVE-2026-110306 encoded scheme is not decoded
FAIL CVE-2026-110180 second @ in host is an error
FAIL CVE-2026-110095 leading .. kept on relative normalize
FAIL CVE-2026-110095 relative path stays relative
$ node fast-uri-selftest.js ./pkg-3.1.9/package
fast-uri 3.1.9
PASS CVE-2026-110306 encoded scheme is not decoded
PASS CVE-2026-110180 second @ in host is an error
PASS CVE-2026-110095 leading .. kept on relative normalize
PASS CVE-2026-110095 relative path stays relative
It does not cover CVE-2026-110725, which depends on the suffix option; search your code for that one (Step 7). Pass a nested copy's path to test it.
Step 7: find direct use in your own code
# Direct imports
grep -rnE "require\(['\"]fast-uri['\"]\)|from ['\"]fast-uri['\"]" --include='*.js' --include='*.mjs' --include='*.cjs' --include='*.ts' src lib app
# The suffix option (CVE-2026-110725)
grep -rnE "reference:\s*['\"]suffix['\"]" --include='*.js' --include='*.ts' .
# Custom ajv URI resolvers, which replace fast-uri
grep -rn "uriResolver" --include='*.js' --include='*.ts' src lib app
At each direct import, ask one question: does the code check one form of the URI and use another?
Step 8: harden the call sites
These are worth doing after the update too, because the same pattern causes bugs with every URL parser:
- Parse once, check the parsed result, use that same result. Do not check the raw string and then render, store or fetch the normalized one.
- Treat a non-empty
erroron a parsed URI as fatal. After the fix, fast-uri reports malformed schemes and hosts through that field. Code that ignores it will still pass bad input along. - Allowlist schemes on the parsed scheme, for example only
http,httpsandmailtofor user links, rather than denylisting dangerous ones on the raw string. - Resolve before you normalize. Resolve a relative reference against its absolute base, then normalize the absolute result, then check it.
- Do not use the suffix option on untrusted input. If you must, validate its output with
parse()before treating it as a URI. - Compare hosts from one parser. If an HTTP client will use the WHATWG parser, check the host from the WHATWG parser too.
- Wrap
resolve()andserialize()in error handling where they take user input. The fixed versions throw on malformed schemes and hosts, and an unhandled exception turns a rejected input into a server error.
If you cannot update today
The advisories' workarounds: reject input whose scheme part contains %; reject an authority with more than one @ or any tab, carriage return or line feed; resolve relative references before normalizing them; avoid the suffix option on untrusted input. These are stopgaps; the update is the fix.
Open questions
- Will NVD add its own scores? All four are "Awaiting Analysis" as of 25 October, so tools that use only NVD scores may show them as unscored.
- Will downstream packages raise their minimum ranges? ajv 8.20.0 still asks for
^3.0.1and@fastify/ajv-compiler4.0.7 for^4.0.0. Both accept the fixed versions but do not force them, so the fix reaches a project only when its lockfile is refreshed. - How fast will installs move? npm's per-version counts for the latest seven days, as returned on 25 October, show 36,123,354 downloads of 3.1.8 against 12,707,012 of 3.1.9, and 1,911,856 of 4.2.2 against 958,599 of 4.2.3. The window overlaps the release day and the counts mix caches and mirrors, but most installs that week were still resolving to older versions.
The fair view
For most applications these are low impact. Two of the four score 3.7, one applies only with a non-default option, and the most common route fast-uri takes into a project, ajv, feeds it schema identifiers you wrote. CISA's assessment on each record says no known exploitation and not automatable, and none of the four is in the KEV catalog.
The maintainers also did careful work for a free library. Fixes for all three supported major lines shipped within 19 minutes of each other, each with new tests, a documented behavior change in the README and workarounds in the advisories, and the releases were published with provenance.
The reason to update anyway is reach. A parser that sits under a validator that sits under a web framework ends up in places nobody listed, and parser differentials are the kind of bug that turns harmless-looking strings into redirects or script URLs in the one application that used the parser differently. Our explainer on URL parser confusion covers the class, and our posts on encoded paths that bypassed middleware in OpenNext and a case-sensitivity gap in a CSS URL sanitizer show the same pattern in other projects this month.
What would have helped
Practices any team can use
- Know your transitive dependencies. Almost nobody chose fast-uri directly. A per-build SBOM turns "do we have fast-uri 3.1.8 anywhere" into a query.
- Refresh lockfiles on a schedule. Caret ranges only help if the lockfile is regenerated. A weekly automated update pull request, with tests, moves fixes like these into your tree without anyone reading the advisory first.
- Do not rely on one advisory feed. For four days the only records were the repository advisories, the CVE records and the OpenJS CNA page. Watching CVE records for packages you depend on closes that gap.
- Rebuild after you update. Images and bundles keep the old copy until rebuilt.
- Check once, use once. The coding habit that defuses all four bugs is to make the security decision on the same parsed value you go on to use.
Where Safeguard fits
Safeguard can help with inventory, prioritization and the update itself. It cannot change how fast-uri parses a string.
- Finding every copy.
safeguard scan --dir .on a repository,safeguard scan --image <name>on a container image, orsafeguard scan --sbom <file>on an existing SBOM produces a dependency inventory with vulnerability matches, written as JSON under./output. In the platform, you can see which projects contain a given component and version, so "every project with fast-uri 3.1.8 or older" is one search across the estate, including the nested copies under ajv and Fastify. - Matching against vulnerability data. Safeguard matches components against vulnerability databases, so these four CVEs show up against the projects that carry an affected copy.
- Ranking by reachability. Reachability analysis is designed to rank the vulnerable code paths your application actually calls first. For a library bug whose impact depends on how it is called, knowing which of your own code paths reach fast-uri is a useful first sort before you review each call site by hand.
- Doing the change with you. The Safeguard CLI's assistant can edit code and run commands with your approval. Asking it to refresh fast-uri in the lockfile, add scoped overrides where a nested copy is pinned, run the test suite and the self test above is the kind of task it handles, with it asking before any command that changes something, under the
/permissionsmode you choose.
Zero-Day Discovery looks for previously unknown flaws in package ecosystems, npm among them, before a CVE exists. It played no part in these four issues, which were reported and fixed by the people credited above.
Where Safeguard does not help: it cannot ship a fast-uri release, and it cannot tell from a version number alone whether your code checks one form of a URI and uses another. That review is still yours.
Common mistakes
- Running
npm installand assuming you are fixed. With a lockfile in place it reinstalls the pinned version. Update, then check withnpm ls fast-uri --all. - Updating one copy and missing the other. Fastify 5 projects usually have a 3.x and a 4.x copy; Fastify 4 projects a 2.x and a 3.x copy.
- Using one unscoped override for every copy. Forcing all copies to
^3.1.9breaks the packages that ask for 4.x. - Accepting
npm audit fix --forceunread. On a project that pins an exact version, npm warns that the fix installs a version "outside the stated dependency range". Read the plan first. - Fixing the repository and forgetting the image. A built container image keeps its copy until you rebuild and redeploy.
- Assuming a version below 2.4.2 or 3.1.3 is safe. Those are outside the CVE-2026-110180 range, but the other three still apply.
- Ignoring the
errorfield or the new exceptions. The fixed versions report malformed schemes and hosts througherror, andresolve()andserialize()now throw on them. Handle both where user input arrives.
FAQ
Which fast-uri version fixes CVE-2026-110306?
2.4.8 on the 2.x line, 3.1.9 on 3.x and 4.2.3 on 4.x, all published on 19 October 2026 (UTC). The same three releases fix CVE-2026-110180, CVE-2026-110725 and CVE-2026-110095.
Is my project affected if I never installed fast-uri?
Probably it contains fast-uri anyway, through ajv 8.17.1 or later or Fastify's packages. Run npm ls fast-uri --all. Through ajv alone, the inputs are usually your own schema identifiers.
Are any of these being exploited?
Not according to the public record. CISA's assessment on each CVE record, added on 20 October, says exploitation "none" and automatable "no", and none of the four is in the KEV catalog as of its 23 October release. EPSS estimates on GitHub's advisory pages were between 0.2% and 0.26% on 25 October.
Why did my scanner not report these on 20 October?
Many scanners read the GitHub Advisory Database or OSV, which added the four advisories on 23 October. Since then, npm audit reports them.
Does the fix change behavior I rely on?
Mostly it rejects input RFC 3986 does not allow: percent-encoded schemes and domain hosts with characters outside the reg-name set now produce an error on parse and make resolve() and serialize() throw. Relative references now keep leading .. segments when normalized, which is a change for valid input. If you had tests that expected the old normalization of relative paths, such as ../x becoming x, they will need updating.
Who reported the vulnerabilities?
The CVE records credit zekth as reporter of CVE-2026-110306, CVE-2026-110180 and CVE-2026-110095, and UlisesGascon as finder of CVE-2026-110725, with UlisesGascon and mcollina as remediation reviewers.
What about the suffix option, CVE-2026-110725?
It applies only if you pass reference: "suffix". That option is not the default, and ajv and the Fastify packages do not set it in the code we read.
How do I know the fixed package I installed is genuine?
Compare the integrity value in your lockfile with the table above and run npm audit signatures; all three fixed versions carry provenance attestations.
If you want to try the scan and fix flow on your own code, the Safeguard apps are at safeguard.sh/download and the CLI guide is at docs.safeguard.sh/docs/cli. For a broader view of tracing a vulnerable component across products, see SBOM incident response: finding affected products fast.
For fast-uri, upstream has already shipped fixes on every supported line, so the risk now sits in the first gap below: the lockfiles, images and bundles that still carry the old copies.
Fix it without waiting for upstream
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 categories include dependencies, licenses, security posture and findings, and each one is set separately. 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.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) 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. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull 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.
See how Safeguard would have helped
These demos are recordings of real sessions on other fixes. None of them shows fast-uri. Each one shows a flow you would use the same way here.
The CLI demo shows the assistant fixing a lodash CVE in a JavaScript repository: it reads the finding and the advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. For fast-uri you would ask the same assistant to move every copy in the lockfile to its fixed line, add a scoped override where a nested copy is pinned, and run your tests and the self test from this post, approving each step.
Click through it at your own pace, or open it full screen.
The Claude demo shows connecting Claude to the Safeguard MCP server with OAuth and choosing which tools Claude can use. Once connected, you could ask Claude which of your projects still carry fast-uri below 2.4.8, 3.1.9 or 4.2.3, and get the list from your own Safeguard data instead of cloning each repository.
Click through it at your own pace, or open it full screen.
The desktop app demo shows a CLI session appearing in the conversation list, asking for work on your computer, picking the folder, and approving or denying each command and file edit until the upgrade is done and the tests pass. For a Fastify service with both a 3.x and a 4.x copy of fast-uri, that is the same flow: pick the service folder, ask for both copies to be updated, and approve the lockfile change and the test run.
Click through it at your own pace, or open it full screen.
Quick check
Your Fastify 5 project's lockfile was written in September. npm ls shows fast-uri 3.1.8 under ajv and 4.2.2 under @fastify/ajv-compiler. What do you update?
Both copies: 3.1.8 to 3.1.9 and 4.2.2 to 4.2.3. Run npm update fast-uri, confirm every copy with npm ls, commit the lockfile, then rebuild and redeploy. Do not force every copy to one version with a single override.
Your app checks that a user-supplied link does not start with javascript: and then stores fast-uri's normalized form. Is CVE-2026-110306 relevant?
Yes. That is the pattern the record describes: the raw string can pass the check while the normalized output carries a live scheme. Update, then check the scheme on the parsed result you actually store, using an allowlist.
Your code never passes the reference: "suffix" option to fast-uri. Which of the four CVEs can you rule out?
CVE-2026-110725. The record says exposure is limited to applications using that non-default option. The other three still apply if untrusted URIs reach fast-uri, so update anyway.
Sources
- CVE-2026-110306 record, CVE Program API
- CVE-2026-110180 record, CVE Program API
- CVE-2026-110725 record, CVE Program API
- CVE-2026-110095 record, CVE Program API
- CVE-2026-86472 record, CVE Program API
- CVE-2026-86818 record, CVE Program API
- GHSA-w74x-v6f9-j3xg, fast-uri repository advisory
- GHSA-wjr9-j795-jxcr, fast-uri repository advisory
- GHSA-333x-w4r6-6crp, fast-uri repository advisory
- GHSA-xhq7-xmqj-769h, fast-uri repository advisory
- GHSA-w74x-v6f9-j3xg in the GitHub Advisory Database
- GHSA-wjr9-j795-jxcr in the GitHub Advisory Database
- GHSA-333x-w4r6-6crp in the GitHub Advisory Database
- GHSA-xhq7-xmqj-769h in the GitHub Advisory Database
- OpenJS Foundation CNA security advisories
- fast-uri 4.2.2 to 4.2.3 comparison
- fast-uri 3.1.8 to 3.1.9 comparison
- fast-uri 2.4.7 to 2.4.8 comparison
- fast-uri on the npm registry
- fast-uri weekly downloads, npm API
- fast-uri per-version downloads, npm API
- ajv weekly downloads, npm API
- ajv on the npm registry
- fastify on the npm registry
- @fastify/ajv-compiler on the npm registry
- fast-json-stringify on the npm registry
- ajv lib/runtime/uri.ts
- ajv lib/compile/resolve.ts
- ajv lib/types/index.ts
- @fastify/ajv-compiler lib/default-ajv-options.js
- fast-json-stringify lib/validator.js
- OSV entry for GHSA-w74x-v6f9-j3xg
- NVD entry for CVE-2026-110306
- CISA Known Exploited Vulnerabilities catalog (JSON feed)
- RFC 3986, Uniform Resource Identifier: Generic Syntax
- WHATWG URL Standard
Last reviewed: 25 October 2026. Check the linked advisories and the npm registry for the latest status.