待翻譯:Cookies leaked to the wrong host: a URL parsing differential in tough-cookie
AI 服務暫時不可用,以下為來源摘要,待恢復後補全翻譯:A cookie-leak bug we reported in tough-cookie We reported a cookie-leak bug in tough-cookie, a library pulled about 480 million times a month: a URL-parsing quirk let cookies scoped to victim.com leak to an attacker’s h…
AI 服務暫時不可用,以下為來源正文,待恢復後補全翻譯。
A cookie-leak bug we reported in tough-cookie We reported a cookie-leak bug in tough-cookie, a library pulled about 480 million times a month: a URL-parsing quirk let cookies scoped to victim.com leak to an attacker’s host. We scored it 7.6 (High). The maintainers shipped our exact one-line fix in v6.0.2 the day after they acknowledged our report. On this page: the bug · the root cause · proof of concept · who is affected · the fix · the disclosure timeline · what it says about testing The bug in one sentence tough-cookie decoded the URL before parsing it, so a crafted URL that a normal parser reads as pointing to the attacker was read by tough-cookie as pointing to the victim. The cookie jar then handed the victim’s cookies to a request bound for the attacker’s server. FieldValue Packagetough-cookie (npm) Reach~480 million downloads a month Affected2.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 The root cause: decodeURI and a backslash tough-cookie is the cookie jar behind a huge slice of the JavaScript ecosystem. It is downloaded around 480 million times a month and rides inside request, got, and axios cookie jars, plus a long tail of OAuth and scraping libraries. Its job is to decide which stored cookies belong on an outgoing request, which makes correct host parsing a security boundary, not a nicety. The jar resolved the request URL with a decode step before parsing: // tough-cookie, before the fix return new URL(decodeURI(url)) Why the escape matters decodeURI turns the percent-escape %5C into a backslash. And per the WHATWG URL standard, a backslash is treated like a forward slash inside a special-scheme URL such as https. That single substitution moves the boundary between the userinfo (the part before the @) and the host. The two hostnames Take the crafted URL https://victim.com%[email protected]/: A normal new URL(url) reads the host as evil.com. That is where the bytes actually go. tough-cookie, after decodeURI, read the host as victim.com, and 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') const jar = new CookieJar() // victim's session cookie await jar.setCookie('session=SECRET; HttpOnly; Path=/', 'https://victim.com/') const crafted = 'https://victim.com%[email protected]/' // tough-cookie attaches the victim's cookie to the crafted URL console.log(await jar.getCookieString(crafted)) // "session=SECRET" console.log(new URL(crafted).hostname) // "evil.com" <- where it really goes The reverse: cookie fixation The same differential works backwards. An attacker who can set a cookie on the crafted URL plants it under victim.com: await jar.setCookie('session=ATTACKER; Path=/', crafted) console.log(await jar.getCookieString('https://victim.com/')) // "session=ATTACKER" Now the attacker’s session rides to the victim’s domain, the classic setup for cookie fixation. DirectionWhat the attacker gets Leakthe victim’s real session cookie, exfiltrated to the attacker host Fixationthe attacker’s cookie planted on the victim’s domain Who is affected Any application using tough-cookie’s CookieJar where an attacker can influence a URL. That surface is larger than it sounds: it opens up whenever you follow HTTP redirects, whenever a URL is user-controlled, and in many SSRF situations. Because tough-cookie is a transitive dependency of so many 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 return new URL(url) That is exactly what shipped. The maintainer’s pull request describes it 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 later. The whole fix is deleting a function call, which is the tell of a good bug: the 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 of it verifiable on GitHub. We reported the bug through Tidelift’s coordinated-disclosure process on July 4, 2026, with the root cause, the both-directions proof of concept above, and the one-line fix already identified. It was acknowledged on July 6. Later that same day the maintainers committed the fix (82de17df), opened PR #614, and released it as v6.0.2 the next day. The fix landed the day after the acknowledgment, roughly forty-seven hours after our report. One detail we appreciated: the regression tests added in the fix use the same %5C@ payload pattern as our proof of concept, so both sides were clearly looking at the same bug. The fast turnaround protected users quickly, which is what matters most, and the maintainers deserve credit for moving. The thread, redacted Here is the correspondence itself. We have blacked out the other party’s name and address, and our own personal address, because the point is the process, not any individual. Nothing else is altered. Our report, July 4, 6:12 PM, sent to the coordinated-disclosure address, with the root cause, the both-directions proof of concept, and the one-line fix: The reply chain: the acknowledgment, our answer, and the follow-up we sent a month after the patch shipped asking for acceptance, a CVE, credit, and an advisory ETA: The acknowledgment, July 6, 8:46 AM: Thank you for the report, we’ll investigate according to the process described at [security process] and let you know the outcome. If the maintainers wish to include you directly, are you okay to be cc’d with them? Our reply, the same morning: Yes, that’s fine. Feel free to CC me in discussions with the maintainers. We were never cc’d. The fix was committed later that same day, and released the next. On August 4, a month after the patch shipped, we wrote back asking to close out four things: whether the report was formally accepted, whether a CVE would be assigned, credit, and an ETA for a public advisory. Credit: given the chain above (report, then acknowledgment, then fix), I’d like to be listed as the finder in the advisory. and, plainly: Getting acknowledgement is a great motivator for researchers like me, its very sad to see it being fixed with no acknowledgement! That request is still open at the time of publishing. Credit is still pending, and it matters As of publication the public fix carries no credit line and no CVE has been assigned. We have asked, through the same process, to be listed as the finder in the advisory, and that request is still open. This is not a complaint about one project: they moved fast, and that is good. It is a general point worth repeating, because it is easy to drop under deadline. Coordinated disclosure runs on credit. Reporting a serious bug privately, for free, with a fix attached is the responsible choice, and a line in the advisory is most of what the researcher gets back for it. Crediting the people who hand you a fix costs nothing and keeps the next researcher reporting. What this says about our testing We publish findings like this, alongside the critical velocity.js RCE and the JSONPath-Plus RCE (both publicly credited to Ryan Cruz), because depth is the one thing a security vendor cannot fake. This class of bug, a parsing differential between two libraries that both believe they are correct, is exactly what a scanner misses and a human reading intent against behavior catches. Hackers stay in the loop AI does the coverage no human team can afford monthly; a human confirms the finding reaches a real security boundary, writes the working proof, and files the disclosure. That white-box reading, pointed at your app every month, is what our subscription buys, and the benchmark runs are public. The pricing is public too: $299 a month for early-stage startups, $2,999 for everyone else, against the $5,000 to $30,000 a single traditional test costs. A startup’s year of continuous testing plus an independent SOC 2 attestation lands near $6,088. The short version We reported a cookie-leak bug in tough-cookie (~480 million downloads a month), scored 7.6, fixed in 6.0.2. Root cause: decodeURI turned %5C into a backslash, the URL parser read it as a slash, and the host boundary moved from evil.com to victim.com. Result: victim cookies leak to attacker hosts, and attacker cookies can be fixated onto victim domains. Fix: delete the decodeURI call. Upgrade to 6.0.2. It was fixed the day after we were acknowledged; credit in the advisory and a CVE are still pending. Credit the researchers who report responsibly. See our other findings and how our testing works. Frequently asked questions Am I affected, and how do I fix it? You are at risk if you use tough-cookie's CookieJar (directly, or through request, got, or axios with a cookie jar) on a version at or before 6.0.1 and an attacker can influence a URL you pass it, which is common when you follow redirects or accept user-supplied URLs. The fix is to upgrade to 6.0.2 or later. Run npm ls tough-cookie to catch old copies pinned by a transitive dependency. What actually goes wrong? tough-cookie ran decodeURI() on the URL before parsing it. decodeURI turns the escape %5C into a backslash, and the WHATWG URL parser treats a backslash like a forward slash inside a special-scheme URL, which moves the boundary between the userinfo and the host. For https://victim.com%[email protected]/, a normal parser sees the host as evil.com, but tough-cookie saw victim.com, so it attached victim.com's cookies to a request that actually goes to evil.com. If there is no credit, how do we know you reported it? The correspondence is quoted in the post, with the other party's name redacted. We reported it through the coordinated-disclosure process on July 4, it was acknowledged on July 6, and the fix shipped the next day as v6.0.2. The fix is the exact change we recommended, and the regression tests added in it use the same %5C@ payload pattern as our proof of concept. What is still pending is public credit in the advisory and a CVE. Our other findings, the velocity.js and JSONPath-Plus RCEs, are publicly credited to Ryan Cruz. Why publish this if it is already patched? Two reasons. First, a patch only helps people who upgrade, and a library this widely depended on takes months to propagate through transitive dependencies, so the warning is still useful. Second, to make a point about credit that the security ecosystem depends on. Both matter. How does HackZero find bugs like this and still charge $299 a month? We own the whole testing stack, so the marginal cost of a run is compute plus senior review, not tester-weeks. The attack agents, the exploitation tooling, and the reporting are all built in-house: AI does the continuous coverage no human team can afford monthly, and hackers stay in the loop to confirm a finding is real, reduce it, and report it responsibly. Owning the stack is also why one subscription can fold in SOC 2 controls and connect you with an independent AICPA-member CPA who attests them. Startups pay $299 a month, every other company $2,999.