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:
parent
9bd5c08d5a
commit
97d876f6db
2 changed files with 61 additions and 48 deletions
|
@ -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"));
|
||||||
|
|
|
@ -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" ].`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue