diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js index 0c1b2b9dd8..978095c6a1 100644 --- a/chrome/content/zotero/xpcom/http.js +++ b/chrome/content/zotero/xpcom/http.js @@ -294,7 +294,15 @@ Zotero.HTTP = new function () { if (!options.foreground) { xmlhttp.mozBackgroundRequest = true; } - xmlhttp.open(method, url, true, options.username, options.password); + // Necko percent-decodes credentials passed to open(), so encode them to preserve + // literal %HH sequences in the password + xmlhttp.open( + method, + url, + true, + options.username && encodeURIComponent(options.username), + options.password && encodeURIComponent(options.password) + ); // Isolate cookies into a separate jar via userContextId if (options.userContextId && xmlhttp.setOriginAttributes) { diff --git a/test/tests/httpTest.js b/test/tests/httpTest.js index 4986a93415..3f9b6a2da5 100644 --- a/test/tests/httpTest.js +++ b/test/tests/httpTest.js @@ -625,6 +625,21 @@ describe("Zotero.HTTP", function () { xmlhttp.getResponseHeader("X-Echo-Auth") ); }); + + it("should preserve literal %HH sequences in the password in request() and download()", async function () { + let username = "user"; + let password = "example%41pass"; + let expected = "Basic " + btoa(username + ":" + password); + let url = `http://${username}:${encodeURIComponent(password)}` + + `@127.0.0.1:${port}/download/auth`; + + let xmlhttp = await Zotero.HTTP.request("GET", url); + assert.equal(xmlhttp.getResponseHeader("X-Echo-Auth"), expected); + + let dest = PathUtils.join(tmpDir, "auth-percent.bin"); + let req = await Zotero.HTTP.download(Services.io.newURI(url), dest); + assert.equal(req.headers.get("X-Echo-Auth"), expected); + }); });