Bug 1697292 - Update the WebExtensions' multipart/form-data parser to match Chrome. r=mixedpuppy
authorAndreu Botella <abb@randomunok.com>
Mon, 24 May 2021 13:17:57 +0000
changeset 580517 6daedfe5f8fea1d6fb4b631d3aaae3d0078b355d
parent 580516 3e05faaa217db6f534c4c62da4a5f32d4bcbcba3
child 580518 f4e6c9f9222e429bfb4b27a056f96ac99fb2b66c
push id38488
push usernbeleuzu@mozilla.com
push dateMon, 24 May 2021 21:58:32 +0000
treeherdermozilla-central@9635f4a632cb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1697292, 1686765
milestone90.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1697292 - Update the WebExtensions' multipart/form-data parser to match Chrome. r=mixedpuppy This will also make the parser compatible with the changes to the multipart/form-data serializer in bug 1686765. Differential Revision: https://phabricator.services.mozilla.com/D108679
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;
   }