Incorporating https://phabricator.services.mozilla.com/D108679 in the try stack draft
authorAndreu Botella <abb@randomunok.com>
Mon, 19 Apr 2021 18:50:59 +0200
changeset 3667641 a06dadddf1ca72798e0f1a1cf4289629c7cb1fe0
parent 3667640 7106bbf1391e9248e56a618467a5b0d9e084f283
child 3667642 138e31daadffae49569f699282a367c34271bd59
push id683128
push userabb@randomunok.com
push dateMon, 19 Apr 2021 16:51:42 +0000
treeherdertry@138e31daadff [default view] [failures only]
milestone89.0a1
Incorporating https://phabricator.services.mozilla.com/D108679 in the try stack
toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html
toolkit/components/extensions/webrequest/WebRequestUpload.jsm
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html
@@ -10,17 +10,17 @@
 </head>
 <body>
 
 <form method="post"
   action="file_WebRequest_page3.html?trigger=form"
   target="_blank"
   enctype="multipart/form-data"
   >
-<input type="text" name="&quot;special&quot; ch�rs" value="sp�cial">
+<input type="text" name="&quot;special&quot; &#x0D;&#x0A; ch�rs" value="sp�cial">
 <input type="file" name="testFile">
 <input type="file" name="emptyFile">
 <input type="text" name="textInput1" value="value1">
 </form>
 
 <form method="post"
   action="file_WebRequest_page3.html?trigger=form"
   target="_blank"
@@ -201,13 +201,65 @@ add_task(async function test_xhr_forms()
     action.searchParams.set("upload", `${blob.content.size} bytes`);
     await post(blob.content);
 
     let byteLength = 16;
     action.searchParams.set("upload", `${byteLength} bytes`);
     await post(new ArrayBuffer(byteLength));
   }
 
+  // Testing the decoding of percent escapes even in cases where the
+  // multipart/form-data serializer won't emit them.
+  {
+    let boundary = "-".repeat(27);
+    for (let i = 0; i < 3; i++) {
+      const randomNumber = Math.floor(Math.random() * (2 ** 32));
+      boundary += String(randomNumber);
+    }
+
+    const formPayload = [
+      `--${boundary}`,
+      'Content-Disposition: form-data; name="percent escapes other than%20quotes and newlines"',
+      "",
+      "",
+      `--${boundary}`,
+      'Content-Disposition: form-data; name="valid UTF-8: %F0%9F%92%A9"',
+      "",
+      "",
+      `--${boundary}`,
+      'Content-Disposition: form-data; name="broken UTF-8: %F0%9F %92%A9"',
+      "",
+      "",
+      `--${boundary}`,
+      'Content-Disposition: form-data; name="percent escapes aren\'t decoded in filenames"; filename="%0D%0A%22"',
+      "Content-Type: application/octet-stream",
+      "",
+      "",
+      `--${boundary}--`,
+      ""
+    ].join("\r\n");
+
+    const action = new URL("file_WebRequest_page3.html?trigger=form", document.location.href);
+    action.searchParams.set("xhr", "1");
+    action.searchParams.set("upload", JSON.stringify({
+      "percent escapes other than quotes and newlines": [""],
+      "valid UTF-8: 💩": [""],
+      "broken UTF-8: � ��": [""],
+      "percent escapes aren't decoded in filenames": ["%0D%0A%22"]
+    }));
+    action.searchParams.set("enctype", "multipart/form-data");
+
+    await fetch(
+      action.href,
+      {
+        method: "POST",
+        headers: {"Content-Type": `multipart/form-data; boundary=${boundary}`},
+        body: formPayload
+      },
+    );
+    await doneAndTabClosed();
+  }
+
   await extension.unload();
 });
 </script>
 </body>
 </html>
--- a/toolkit/components/extensions/webrequest/WebRequestUpload.jsm
+++ b/toolkit/components/extensions/webrequest/WebRequestUpload.jsm
@@ -346,19 +346,31 @@ function parseFormData(stream, channel, 
         !name ||
         headers.getParam("content-disposition", "") !== "form-data"
       ) {
         throw new Error(
           "Invalid MIME stream: No valid Content-Disposition header"
         );
       }
 
+      // Decode the percent-escapes in the name. Unlike with decodeURIComponent,
+      // partial percent-escapes are passed through as is rather than throwing
+      // exceptions.
+      name = name.replace(/(%[0-9A-Fa-f]{2})+/g, match => {
+        const bytes = new Uint8Array(match.length / 3);
+        for (let i = 0; i < match.length / 3; i++) {
+          bytes[i] = parseInt(match.substring(i * 3 + 1, (i + 1) * 3), 16);
+        }
+        return new TextDecoder("utf-8").decode(bytes);
+      });
+
       if (headers.has("content-type")) {
         // For file upload fields, we return the filename, rather than the
-        // file data.
+        // file data. We're following Chrome in not percent-decoding the
+        // filename.
         let filename = headers.getParam("content-disposition", "filename");
         content = filename || "";
       }
       formData.get(name).push(content);
     }
 
     return formData;
   }