diff --git a/tests/page/source_login.mjs b/tests/page/source_login.mjs index dced308..de8ab47 100644 --- a/tests/page/source_login.mjs +++ b/tests/page/source_login.mjs @@ -42,10 +42,8 @@ for (const [url, address, user, pass] of [ ["https://alice:100%done@repo.example/private/repo", "https://repo.example/private/repo", "alice", "100%done"], ["https:\t//alice:s3cret@repo.example/repo", "https://repo.example/repo", "alice", "s3cret"], ["ht\ntps://alice:s3cret@repo.example/repo", "https://repo.example/repo", "alice", "s3cret"], - ["https:\\\\alice:s3cret@repo.example/repo", "https://repo.example/repo", "alice", "s3cret"], ["https:alice:s3cret@repo.example/repo", "https://repo.example/repo", "alice", "s3cret"], ["\u0000https://alice:s3cret@repo.example/repo", "https://repo.example/repo", "alice", "s3cret"], - ["fdroidrepos://alice:p\\w@repo.example/repo", "fdroidrepos://repo.example/repo", "alice", "p\\w"], ["https://alice:%FF@repo.example/private/repo", "https://repo.example/private/repo", "alice", "%FF"], ["https://al%20ice:p%40ss:word@repo.example/r", "https://repo.example/r", "al ice", "p@ss:word"], ["https://alice:p%FF\u00e4@repo.example/repo", "https://repo.example/repo", "alice", "p%FF\u00e4"], @@ -60,7 +58,7 @@ for (const [url, address, user, pass] of [ } // An address the parser rejects that may hold a login is refused and left as typed, never sent. -for (const url of ["https://alice:s3cret@repo example/repo", "https:alice:s3cret@repo example/repo", "https://repo.example:bad\\a@b"]) { +for (const url of ["https://alice\\name:pw@repo.example/repo", "https:\\\\alice:s3cret@repo.example/repo", "fdroidrepos://alice:p\\w@repo.example/repo", "https://alice:s3cret@repo example/repo", "https:alice:s3cret@repo example/repo", "https://repo.example:bad\\a@b"]) { f = form(url); if (moveLogin(f) !== false) fail("accepted an invalid address with a login: " + url); if (f.elements.username.value || f.elements.password.value) fail("guessed a login from an invalid address: " + url); @@ -75,4 +73,7 @@ for (const url of ["https://repo.example/repo", "not a url", "https://repo.examp const html = sourceToken({ id: "github", name: "GitHub releases", token: { set: true, from: "saved", rejected: true } }); if (!html.includes("rejected") || !html.includes('data-clear-token="github"')) fail("token row: " + html); if (sourceToken({ id: "fdroid", name: "F-Droid" }) !== "") fail("token field on a source without one"); +const slow = "https://a:" + "%FF".repeat(4000) + "@repo.example/r", t0 = Date.now(); +f = form(slow); moveLogin(f); +if (Date.now() - t0 > 200 || f.elements.password.value !== "%FF".repeat(4000)) fail("decoding malformed escapes is slow or wrong"); console.log("ok"); diff --git a/ui/index.html b/ui/index.html index 4d76b46..ebca9d4 100644 --- a/ui/index.html +++ b/ui/index.html @@ -3293,13 +3293,15 @@ async function addSource(body) { // Plain text, not the URL parser: an address it rejects (or a login it can't decode) must // still lose its login first. Decoding falls back to the text as typed. // Percent-decode what decodes; keep any byte sequence that isn't UTF-8 as typed. +// One forward pass: each lead byte gives its sequence length (at most 4 bytes). function decodeLenient(s) { return s.replace(/(?:%[0-9a-f]{2})+/gi, run => { - let out='', i=0; - while(ii; j-=3){ try { out+=decodeURIComponent(run.slice(i,j)); break; } catch {} } - if(j>i) i=j; else { out+=run.slice(i,i+3); i+=3; } + const b=run.match(/%[0-9a-f]{2}/gi); let out='', i=0; + while(i=0xC2&&v<0xE0 ? 2 : v>=0xE0&&v<0xF0 ? 3 : v>=0xF0&&v<0xF5 ? 4 : 0; + if(n){ try { out+=decodeURIComponent(b.slice(i,i+n).join('')); i+=n; continue; } catch {} } + out+=b[i]; i++; } return out; }); @@ -3311,6 +3313,9 @@ function moveLogin(form) { const el=form.elements; el.url.value=el.url.value.replace(/^[\u0000- ]+|[\u0000- ]+$/g,'').replace(/[\t\n\r]/g,''); let u; + // A backslash and an '@' together are read differently by browsers and by the + // server, so the address can't be split safely: refuse it rather than guess. + if(el.url.value.includes('@') && el.url.value.includes('\\'))return false; try { u=new URL(el.url.value); } catch { return !el.url.value.includes('@'); } if(!u.username && !u.password)return true; const user=decodeLenient(u.username), pass=decodeLenient(u.password);