Skip to main content
VulnerabilityPublished

CVE-2026-126354: Mikutter · Addressable URI normalization resolves some dotted path segments to the wrong path

Affects mikutterSource GCVE Database

0
Compare
Authoritative record:CVE-2026-126354· Medium 4.8

Not known to be exploited · CVSS 4.8 Medium · fixed in 2.9.1 : update mikutter when convenient.

Data as of 13 Nov 2026, 21:02 UTC

CVSS 3.1 base score
4.8Medium
How severe the flaw is on the 0–10 CVSS scale.

Thin evidence

One scorer or none
CVSS scores by source
CNA (GitHub_M)CVSS 3.14.8
Patch
Available – fixed in 2.9.1
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N

Exploitation evidence

  1. Disclosed: evidence found.

    CVE published 12 Nov 2026

  2. Detectable: no evidence on record.

    No public nuclei template indexed

  3. Weaponized: no evidence on record.

    No public exploit code indexed in Metasploit or Exploit-DB

  4. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  5. Observed by OffSeq Mirage: no evidence on record.

    No attack traffic recorded by OffSeq Mirage sensors

A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 13 Nov 2026.

What to do

Affected software

ProductAffectedFixed in
Homebrewmikutterpkg:brew/mikutterOther threatsAffected
  • >=3.4.6
Fixed in–

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

Description

### Summary `Addressable::URI.normalize_path` resolves some paths containing `..` segments to a different path than RFC 3986 does. When a path climbs above the root before any other `..` is resolved, the segments that later `..` segments should remove are instead kept: `/../../public/x/../../secret` normalizes to `/public/x/secret` instead of `/secret`. `URI#normalize`, `URI#join`, `URI#==` and `URI#===` all use this normalization. An application that checks a normalized Addressable path against an allowlist, but sends the original URI to something that resolves it according to RFC 3986, can be made to allow a request for a path outside the allowlist. ### Details `normalize_path` in 2.9.0 (`lib/addressable/uri.rb`) removes dot segments by repeating a set of rewrites until nothing changes. On each pass it finds the first `/segment/../` pair with `RULE_2B_2C`, then replaces every textual copy of that pair in the path with `/`: ```ruby pair = normalized_path.match(RULE_2B_2C) ... regexp = "/#{Regexp.escape(parent.to_s)}/\\.\\./|" regexp += "(/#{Regexp.escape(current.to_s)}/\\.\\.$)" ... mod ||= normalized_path.gsub!(Regexp.new(regexp), SLASH) ``` When the first pair found is `/../../`, where the "segment" is itself `..`, every `/../../` in the path is replaced with `/`. That includes copies later in the path, where each `..` should have removed the segment before it. For `/../../public/x/../../secret`: - RFC 3986 §5.2.4 drops the two leading `..` segments, which can't go above the root, then removes `x` and `public`, giving `/secret`. - 2.9.0 replaces both copies of `/../../` with `/` in one step, giving `/public/x/secret`. Paths where an ordinary `/segment/../` pair comes first, such as `/a/../../b/c/../../d`, are resolved correctly. This was introduced in 2.2.5. Versions 2.0.0 through 2.2.4 and earlier versions resolve these paths correctly. It is fixed in 2.9.1, which replaces the rewrite loop with a single pass that correctly follows RFC 3986 §5.2.4. ### Proof of concept ```ruby require "addressable/uri" require "uri" url = "http://example.com/../../public/x/../../secret" # Addressable's normalized path, compared with RFC 3986 resolution by Ruby's URI library puts Addressable::URI.parse(url).normalize.path puts URI.join("http://example.com/", "../../public/x/../../secret").path # join and == use the same normalization puts Addressable::URI.join("http://example.com/", "../../public/x/../../secret").path puts Addressable::URI.parse(url) == Addressable::URI.parse("http://example.com/public/x/secret") # An allowlist check on the normalized path passes, but the request reaches /secret def allowed?(url) = Addressable::URI.parse(url).normalize.path.start_with?("/public/") puts allowed?(url) ``` Output with Addressable 2.9.0: ``` /public/x/secret /secret /public/x/secret true true ``` Output with Addressable 2.9.1: ``` /secret /secret /secret false false ``` ### Impact This is an interpretation conflict (CWE-436) between Addressable and other RFC 3986 resolvers, which could lead to path traversal past an allowlist (CWE-22). An application is affected when it does both of these: - makes an access decision on a path normalized or joined by Addressable, such as "only paths under `/public/` may be proxied" or "only these redirect targets are allowed"; - sends the original, unnormalized URI to a component that resolves dot segments itself, such as an HTTP client, a proxy, a web server or a browser. An attacker may be able to bypass a check by exploiting behavioral differences between URI parsers. Impact from this is application-dependent. Applications that use Addressable's normalized form for both the check and the request are unaffected, as are applications that reject paths containing `..` segments. As a work-around for unpatched versions, before making access decisions on a path, either reject any containing `..` segments, including percent-encoded forms such as `%2e%2e`, or use the same normalized value for both the check and the request.

References

Community reviews

Loading reviews…