1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-11 08:33:43 -05:00

fix(std/http): Don't use assert() for user input validation (#6092)

This commit is contained in:
Nayeem Rahman 2020-06-04 03:32:27 +01:00 committed by GitHub
parent 9bd5c08d5a
commit 97d876f6db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 61 additions and 48 deletions

View file

@ -118,27 +118,37 @@ function isProhibidedForTrailer(key: string): boolean {
return s.has(key.toLowerCase()); return s.has(key.toLowerCase());
} }
/** /** Read trailer headers from reader and append values to headers. "trailer"
* Read trailer headers from reader and append values to headers. * field will be deleted. */
* "trailer" field will be deleted.
* */
export async function readTrailers( export async function readTrailers(
headers: Headers, headers: Headers,
r: BufReader r: BufReader
): Promise<void> { ): Promise<void> {
const headerKeys = parseTrailer(headers.get("trailer")); const trailers = parseTrailer(headers.get("trailer"));
if (!headerKeys) return; if (trailers == null) return;
const trailerNames = [...trailers.keys()];
const tp = new TextProtoReader(r); const tp = new TextProtoReader(r);
const result = await tp.readMIMEHeader(); const result = await tp.readMIMEHeader();
assert(result !== null, "trailer must be set"); if (result == null) {
throw new Deno.errors.InvalidData("Missing trailer header.");
}
const undeclared = [...result.keys()].filter(
(k) => !trailerNames.includes(k)
);
if (undeclared.length > 0) {
throw new Deno.errors.InvalidData(
`Undeclared trailers: ${Deno.inspect(undeclared)}.`
);
}
for (const [k, v] of result) { for (const [k, v] of result) {
if (!headerKeys.has(k)) {
throw new Error("Undeclared trailer field");
}
headerKeys.delete(k);
headers.append(k, v); headers.append(k, v);
} }
assert(Array.from(headerKeys).length === 0, "Missing trailers"); const missingTrailers = trailerNames.filter((k) => !result.has(k));
if (missingTrailers.length > 0) {
throw new Deno.errors.InvalidData(
`Missing trailers: ${Deno.inspect(missingTrailers)}.`
);
}
headers.delete("trailer"); headers.delete("trailer");
} }
@ -146,16 +156,17 @@ function parseTrailer(field: string | null): Headers | undefined {
if (field == null) { if (field == null) {
return undefined; return undefined;
} }
const keys = field.split(",").map((v) => v.trim().toLowerCase()); const trailerNames = field.split(",").map((v) => v.trim().toLowerCase());
if (keys.length === 0) { if (trailerNames.length === 0) {
throw new Error("Empty trailer"); throw new Deno.errors.InvalidData("Empty trailer header.");
} }
for (const key of keys) { const prohibited = trailerNames.filter((k) => isProhibidedForTrailer(k));
if (isProhibidedForTrailer(key)) { if (prohibited.length > 0) {
throw new Error(`Prohibited field for trailer`); throw new Deno.errors.InvalidData(
} `Prohibited trailer names: ${Deno.inspect(prohibited)}.`
);
} }
return new Headers(keys.map((key) => [key, ""])); return new Headers(trailerNames.map((key) => [key, ""]));
} }
export async function writeChunkedBody( export async function writeChunkedBody(
@ -176,7 +187,8 @@ export async function writeChunkedBody(
await writer.write(endChunk); await writer.write(endChunk);
} }
/** write trailer headers to writer. it mostly should be called after writeResponse */ /** Write trailer headers to writer. It should mostly should be called after
* `writeResponse()`. */
export async function writeTrailers( export async function writeTrailers(
w: Deno.Writer, w: Deno.Writer,
headers: Headers, headers: Headers,
@ -184,29 +196,31 @@ export async function writeTrailers(
): Promise<void> { ): Promise<void> {
const trailer = headers.get("trailer"); const trailer = headers.get("trailer");
if (trailer === null) { if (trailer === null) {
throw new Error('response headers must have "trailer" header field'); throw new TypeError("Missing trailer header.");
} }
const transferEncoding = headers.get("transfer-encoding"); const transferEncoding = headers.get("transfer-encoding");
if (transferEncoding === null || !transferEncoding.match(/^chunked/)) { if (transferEncoding === null || !transferEncoding.match(/^chunked/)) {
throw new Error( throw new TypeError(
`trailer headers is only allowed for "transfer-encoding: chunked": got "${transferEncoding}"` `Trailers are only allowed for "transfer-encoding: chunked", got "transfer-encoding: ${transferEncoding}".`
); );
} }
const writer = BufWriter.create(w); const writer = BufWriter.create(w);
const trailerHeaderFields = trailer const trailerNames = trailer.split(",").map((s) => s.trim().toLowerCase());
.split(",") const prohibitedTrailers = trailerNames.filter((k) =>
.map((s) => s.trim().toLowerCase()); isProhibidedForTrailer(k)
for (const f of trailerHeaderFields) { );
assert( if (prohibitedTrailers.length > 0) {
!isProhibidedForTrailer(f), throw new TypeError(
`"${f}" is prohibited for trailer header` `Prohibited trailer names: ${Deno.inspect(prohibitedTrailers)}.`
); );
} }
const undeclared = [...trailers.keys()].filter(
(k) => !trailerNames.includes(k)
);
if (undeclared.length > 0) {
throw new TypeError(`Undeclared trailers: ${Deno.inspect(undeclared)}.`);
}
for (const [key, value] of trailers) { for (const [key, value] of trailers) {
assert(
trailerHeaderFields.includes(key),
`Not trailer header field: ${key}`
);
await writer.write(encoder.encode(`${key}: ${value}\r\n`)); await writer.write(encoder.encode(`${key}: ${value}\r\n`));
} }
await writer.write(encoder.encode("\r\n")); await writer.write(encoder.encode("\r\n"));

View file

@ -1,5 +1,4 @@
import { import {
AssertionError,
assertThrowsAsync, assertThrowsAsync,
assertEquals, assertEquals,
assert, assert,
@ -105,8 +104,8 @@ test("readTrailer should throw if undeclared headers found in trailer", async ()
async () => { async () => {
await readTrailers(h, new BufReader(new Buffer(encode(trailer)))); await readTrailers(h, new BufReader(new Buffer(encode(trailer))));
}, },
Error, Deno.errors.InvalidData,
"Undeclared trailer field" `Undeclared trailers: [ "`
); );
} }
}); });
@ -120,8 +119,8 @@ test("readTrailer should throw if trailer contains prohibited fields", async ()
async () => { async () => {
await readTrailers(h, new BufReader(new Buffer())); await readTrailers(h, new BufReader(new Buffer()));
}, },
Error, Deno.errors.InvalidData,
"Prohibited field for trailer" `Prohibited trailer names: [ "`
); );
} }
}); });
@ -145,15 +144,15 @@ test("writeTrailer should throw", async () => {
() => { () => {
return writeTrailers(w, new Headers(), new Headers()); return writeTrailers(w, new Headers(), new Headers());
}, },
Error, TypeError,
'must have "trailer"' "Missing trailer header."
); );
await assertThrowsAsync( await assertThrowsAsync(
() => { () => {
return writeTrailers(w, new Headers({ trailer: "deno" }), new Headers()); return writeTrailers(w, new Headers({ trailer: "deno" }), new Headers());
}, },
Error, TypeError,
"only allowed" `Trailers are only allowed for "transfer-encoding: chunked", got "transfer-encoding: null".`
); );
for (const f of ["content-length", "trailer", "transfer-encoding"]) { for (const f of ["content-length", "trailer", "transfer-encoding"]) {
await assertThrowsAsync( await assertThrowsAsync(
@ -164,8 +163,8 @@ test("writeTrailer should throw", async () => {
new Headers({ [f]: "1" }) new Headers({ [f]: "1" })
); );
}, },
AssertionError, TypeError,
"prohibited" `Prohibited trailer names: [ "`
); );
} }
await assertThrowsAsync( await assertThrowsAsync(
@ -176,8 +175,8 @@ test("writeTrailer should throw", async () => {
new Headers({ node: "js" }) new Headers({ node: "js" })
); );
}, },
AssertionError, TypeError,
"Not trailer" `Undeclared trailers: [ "node" ].`
); );
}); });