Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(fetch): Correctly decode multipart/form-data names and filenames #19145

Merged
merged 1 commit into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions cli/tests/unit/body_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,59 @@ Deno.test(
},
);

// FormData: non-ASCII names and filenames
Deno.test(
{ permissions: { net: true } },
async function bodyMultipartFormDataNonAsciiNames() {
const boundary = "----01230123";
const payload = [
`--${boundary}`,
`Content-Disposition: form-data; name="文字"`,
"",
"文字",
`--${boundary}`,
`Content-Disposition: form-data; name="file"; filename="文字"`,
"Content-Type: application/octet-stream",
"",
"",
`--${boundary}--`,
].join("\r\n");

const body = buildBody(
new TextEncoder().encode(payload),
new Headers({
"Content-Type": `multipart/form-data; boundary=${boundary}`,
}),
);

const formData = await body.formData();
assert(formData.has("文字"));
assertEquals(formData.get("文字"), "文字");
assert(formData.has("file"));
assert(formData.get("file") instanceof File);
assertEquals((formData.get("file") as File).name, "文字");
},
);

// FormData: non-ASCII names and filenames roundtrip
Deno.test(
{ permissions: { net: true } },
async function bodyMultipartFormDataNonAsciiRoundtrip() {
const inFormData = new FormData();
inFormData.append("文字", "文字");
inFormData.append("file", new File([], "文字"));

const body = buildBody(inFormData);

const formData = await body.formData();
assert(formData.has("文字"));
assertEquals(formData.get("文字"), "文字");
assert(formData.has("file"));
assert(formData.get("file") instanceof File);
assertEquals((formData.get("file") as File).name, "文字");
},
);

Deno.test(
{ permissions: { net: true } },
async function bodyURLEncodedFormData() {
Expand Down
33 changes: 28 additions & 5 deletions ext/fetch/21_formdata.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const {
StringPrototypeReplaceAll,
TypeError,
TypedArrayPrototypeSubarray,
Uint8Array,
} = primordials;

const entryList = Symbol("entry list");
Expand Down Expand Up @@ -358,6 +359,20 @@ function parseContentDisposition(value) {
return params;
}

/**
* Decodes a string containing UTF-8 mistakenly decoded as Latin-1 and
* decodes it correctly.
* @param {string} latin1String
* @returns {string}
*/
function decodeLatin1StringAsUtf8(latin1String) {
const buffer = new Uint8Array(latin1String.length);
for (let i = 0; i < latin1String.length; i++) {
buffer[i] = latin1String.charCodeAt(i);
}
return core.decode(buffer);
}

const CRLF = "\r\n";
const LF = StringPrototypeCodePointAt(CRLF, 1);
const CR = StringPrototypeCodePointAt(CRLF, 0);
Expand Down Expand Up @@ -465,23 +480,31 @@ class MultipartParser {
i - boundaryIndex - 1,
);
// https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata
const filename = MapPrototypeGet(disposition, "filename");
const name = MapPrototypeGet(disposition, "name");
// These are UTF-8 decoded as if it was Latin-1.
// TODO(@andreubotella): Maybe we shouldn't be parsing entry headers
// as Latin-1.
const latin1Filename = MapPrototypeGet(disposition, "filename");
const latin1Name = MapPrototypeGet(disposition, "name");

state = 5;
// Reset
boundaryIndex = 0;
headerText = "";

if (!name) {
if (!latin1Name) {
continue; // Skip, unknown name
}

if (filename) {
const name = decodeLatin1StringAsUtf8(latin1Name);
if (latin1Filename) {
const blob = new Blob([content], {
type: headers.get("Content-Type") || "application/octet-stream",
});
formData.append(name, blob, filename);
formData.append(
name,
blob,
decodeLatin1StringAsUtf8(latin1Filename),
);
} else {
formData.append(name, core.decode(content));
}
Expand Down