Cookies leaked to the wrong host: a URL parsing differential in tough-cookie

cuaupadilla1 pts0 comments

A cookie-leak bug we reported in tough-cookie (480M downloads a month) | HackZero

Compliance<br>Pentest<br>Enterprise<br>Learn<br>Pricing<br>Sign in →<br>Get started

A cookie-leak bug we reported in tough-cookie

We reported a cookie-leak bug in tough-cookie, a library pulled about 480<br>million times a month: a URL-parsing quirk let cookies scoped to<br>victim.com leak to an attacker’s host. We scored it 7.6 (High). The<br>maintainers shipped our exact one-line fix in v6.0.2 the day after they<br>acknowledged our report.

On this page: the bug ·<br>the root cause ·<br>proof of concept · who is affected ·<br>the fix · the disclosure timeline ·<br>what it says about testing

The bug in one sentence

tough-cookie decoded the URL before parsing it, so a crafted URL that a<br>normal parser reads as pointing to the attacker was read by tough-cookie as<br>pointing to the victim. The cookie jar then handed the victim’s cookies to a<br>request bound for the attacker’s server.

FieldValuePackagetough-cookie (npm)Reach~480 million downloads a monthAffected2.5.0 through 6.0.1 (the decodeURI call has been there since 2.5.0)Fixed6.0.2 (PR #614, released July 7, 2026)SeverityHigh, our CVSS score 7.6 (no CVE assigned)ClassURL-parsing differential leading to cookie disclosure<br>The root cause: decodeURI and a backslash

tough-cookie is the cookie jar behind a huge slice of the JavaScript<br>ecosystem. It is downloaded around 480 million times a<br>month and rides inside request,<br>got, and axios cookie jars, plus a long tail of OAuth and scraping<br>libraries. Its job is to decide which stored cookies belong on an outgoing<br>request, which makes correct host parsing a security boundary, not a<br>nicety.

The jar resolved the request URL with a decode step before parsing:

// tough-cookie, before the fix<br>return new URL(decodeURI(url))<br>Why the escape matters

decodeURI<br>turns the percent-escape %5C into a backslash. And per the<br>WHATWG URL standard, a backslash<br>is treated like a forward slash inside a special-scheme URL such as https.<br>That single substitution moves the boundary between the userinfo (the part<br>before the @) and the host.

The two hostnames

Take the crafted URL https://victim.com%5C@evil.com/:

A normal new URL(url) reads the host as evil.com. That is where the<br>bytes actually go.

tough-cookie, after decodeURI, read the host as victim.com, and<br>attached that domain’s cookies.

The request goes to the attacker; the victim’s cookies go with it.

Proof of concept

Both directions reproduce against tough-cookie 6.0.1 from npm.

const { CookieJar } = require('tough-cookie')<br>const jar = new CookieJar()

// victim's session cookie<br>await jar.setCookie('session=SECRET; HttpOnly; Path=/', 'https://victim.com/')

const crafted = 'https://victim.com%5C@evil.com/'

// tough-cookie attaches the victim's cookie to the crafted URL<br>console.log(await jar.getCookieString(crafted)) // "session=SECRET"<br>console.log(new URL(crafted).hostname) // "evil.com"<br>The reverse: cookie fixation

The same differential works backwards. An attacker who can set a cookie on<br>the crafted URL plants it under victim.com:

await jar.setCookie('session=ATTACKER; Path=/', crafted)<br>console.log(await jar.getCookieString('https://victim.com/')) // "session=ATTACKER"<br>Now the attacker’s session rides to the victim’s domain, the classic setup<br>for cookie fixation.

DirectionWhat the attacker getsLeakthe victim’s real session cookie, exfiltrated to the attacker hostFixationthe attacker’s cookie planted on the victim’s domain<br>Who is affected

Any application using tough-cookie’s CookieJar where an attacker can<br>influence a URL. That surface is larger than it sounds: it opens up whenever<br>you follow HTTP redirects, whenever a URL is user-controlled, and in many<br>SSRF situations. Because tough-cookie is a transitive dependency of so many<br>HTTP clients, plenty of teams are exposed without ever having typed its name.

The fix is one line

Remove the decode step and parse the URL as received:

// after the fix<br>return new URL(url)<br>That is exactly what shipped. The maintainer’s pull request describes it<br>plainly:

Decoding the URL can result in incorrect parsing in some cases.

and the change is titled, in the release:

avoid decoding URL authority part

Upgrade to tough-cookie 6.0.2 or<br>later. The<br>whole fix is deleting a function call, which is the tell of a good bug: the<br>security boundary was one transformation away from correct.

The disclosure timeline

We like this finding as a clean provenance story, so here is the record, all<br>of it verifiable on GitHub.

We reported the bug through Tidelift’s coordinated-disclosure process on<br>July 4, 2026, with the root cause, the both-directions proof of concept<br>above, and the one-line fix already identified. It was acknowledged on<br>July 6. Later that same day the maintainers committed the fix<br>(82de17df),<br>opened PR #614, and<br>released it as v6.0.2 the next day. The fix landed the day after the<br>acknowledgment, roughly forty-seven hours after our report.

One detail we appreciated: the regression tests...

cookie tough victim attacker crafted parsing

Related Articles