Security News

Cybersecurity news aggregator

πŸ”“
MEDIUM Vulnerabilities Reddit r/netsec

The Forgotten Bug: How a Node.js Core Design Flaw Enables HTTP Request Splitting

  • What: A Node.js core design flaw enables HTTP request splitting
  • Impact: Developers and applications using Node.js may be affected
Read Full Article →

The Forgotten Bug: How a Node.js Core Design Flaw Enables... RESPONSIBLE DISCLOSURE NOTICE This vulnerability was reported to Node.js through their HackerOne program. The Node.js security team has assessed it and determined it is not a vulnerability under their current threat model . This paper is published to inform the ecosystem and help developers protect their applications. Table of Contents Prologue: A Bug That Won’t Die The 2018 Precedent: CVE-2018-12116 The Root Cause: Anatomy of the TOCTOU Walking Through the Source Code The Impact Spectrum: From Header Injection to Request Splitting The Ecosystem Audit: 7 Vulnerable Libraries Library-by-Library Deep Dive Libraries That Got It Right Live Demo Node.js Response: β€œNot a Vulnerability” Call to Arms 1. Prologue: A Bug That Won’t Die In 2018, a researcher discovered that Node.js’s http.request() would happily pass Unicode characters through to the wire, where latin1 encoding would truncate them into CRLF bytes β€” enabling HTTP Request Splitting. It was assigned CVE-2018-12116 , scored CVSS 7.5 HIGH, and promptly fixed. The fix added a regex validation to reject paths containing characters outside \u0021-\u00ff : // lib/_http_client.js (the 2018 fix, still present today) const INVALID_PATH_REGEX = / [^\u 0021- \u 00ff ] / ; if ( options . path ) { const path = String ( options . path ); if ( INVALID_PATH_REGEX . test ( path )) { throw new ERR_UNESCAPED_CHARACTERS ( ' Request path ' ); } } Case closed. Right? Not quite. The 2018 fix has a fundamental design flaw: it only runs at construction time. The property it validates β€” this.path β€” remains a plain writable JavaScript property with no setter, no proxy, no Object.defineProperty guard. Any code that mutates ClientRequest.path after construction completely bypasses this validation. And as I discovered, an enormous amount of code does exactly that. 2. The 2018 Precedent: CVE-2018-12116 To understand the current bug, we need to understand its predecessor. CVE-2018-12116 β€” HTTP Request Splitting via Unicode Field Value CVE CVE-2018-12116 CVSS 7.5 HIGH Affected Node.js < 6.15.0, < 8.14.0, < 10.14.0, < 11.3.0 Reporter Arkadiy Tetelman (Lob) CWE CWE-115 (Misinterpretation of Input) The Mechanism: Node.js versions 8 and below used latin1 encoding when constructing HTTP requests without a body. Latin1 is a single-byte encoding β€” it can’t represent high Unicode characters, so it truncates them to their lowest byte . An attacker could craft Unicode characters that, when truncated to latin1, produced HTTP control bytes: \u{010D} β†’ \x0D (Carriage Return, \r ) \u{010A} β†’ \x0A (Line Feed, \n ) This meant a path like "/safe\u{010D}\u{010A}\u{010D}\u{010A}GET /admin" would pass any ASCII validation, but on the wire would become "/safe\r\n\r\nGET /admin" β€” a fully split second HTTP request. The Fix: Reject any path containing characters outside the range \u0021-\u00ff at construction time. The fix was effective for its specific attack vector. But it introduced an assumption that would prove dangerous: that validation at construction time is sufficient. 3. The Root Cause: Anatomy of the TOCTOU The vulnerability I found is a classic TOCTOU (Time-of-Check-Time-of-Use) bug. TIME ─────────────────────────────────────────────────────► β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ http.request() β”‚ β”‚ TOCTOU WINDOW β”‚ β”‚ _implicitHeader()β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ options.path β”‚ β”‚ ClientRequest β”‚ β”‚ this.path used β”‚ β”‚ is VALIDATED β”‚ β”‚ is EXPOSED to β”‚ β”‚ directly in β”‚ β”‚ against │────────►│ user code via │───────►│ HTTP request β”‚ β”‚ INVALID_PATH_ β”‚ β”‚ events/callbacks β”‚ β”‚ line β€” NO β”‚ β”‚ REGEX β”‚ β”‚ β”‚ β”‚ re-validation β”‚ β”‚ β”‚ β”‚ .path is a PLAIN β”‚ β”‚ β”‚ β”‚ βœ…βœ… CHECK β”‚ β”‚ WRITABLE property β”‚ β”‚ ❌❌ USE β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ 👩 | β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ATTACKER MUTATES β”‚ β”‚ clientReq.path = β”‚ β”‚ "/x\r\n\r\nGET /" β”‚ β”‚ β”‚ β”‚ Validation is β”‚ β”‚ NEVER re-run β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ In simple terms: CHECK : When you call http.request(options) , Node.js validates options.path against INVALID_PATH_REGEX . If it contains CRLF characters ( \r , \n ) or characters outside \u0021-\u00ff , it throws an error. Good. WINDOW : The resulting ClientRequest object has a .path property that is a plain writable JavaScript property β€” this.path = options.path || '/' . No setter. No Object.defineProperty . No Proxy . Any code with a reference to the object can write to it freely. USE : When the request is actually sent (triggered by .write() , .end() , or .pipe() ), the method _implicitHeader() reads this.path directly and concatenates it into the HTTP request line: this.method + ' ' + this.path + ' HTTP/1.1\r\n' . No re-validation. The gap between step 1 and step 3 is the TOCTOU window. Any mutation of .path during this window bypasses all CRLF validation. 4. Walking Through the Source Code Let’s trace exactly what happens in the Node.js source code. All references are to the current Node.js main branch at time of writing. ...

Share this article