88 lines
5.2 KiB
TypeScript
88 lines
5.2 KiB
TypeScript
import { Readable } from "node:stream";
|
|
import { describe, expect, test } from "vitest";
|
|
|
|
import {
|
|
createLimitedRequestBody,
|
|
DEFAULT_WEB_REQUEST_BODY_BYTES,
|
|
EMAIL_WEBHOOK_REQUEST_BODY_BYTES,
|
|
MAX_WEB_REQUEST_BODY_BYTES,
|
|
OAUTH_REGISTRATION_REQUEST_BODY_BYTES,
|
|
parseRequestContentLength,
|
|
PORTAL_UPLOAD_REQUEST_BODY_BYTES,
|
|
PORTAL_UPLOAD_MULTIPART_OVERHEAD_BYTES,
|
|
PUBLIC_AUTH_FORM_REQUEST_BODY_BYTES,
|
|
PUBLIC_BOOKING_REQUEST_BODY_BYTES,
|
|
PUBLIC_FORM_REQUEST_BODY_BYTES,
|
|
RequestBodyTooLargeError,
|
|
resolveWebRequestBodyLimit,
|
|
} from "./request-body";
|
|
|
|
describe("web request body limit", () => {
|
|
test("parses only safe decimal content lengths", () => {
|
|
expect(parseRequestContentLength("42")).toBe(42);
|
|
expect(parseRequestContentLength(" 42 ")).toBe(42);
|
|
expect(parseRequestContentLength("-1")).toBeNull();
|
|
expect(parseRequestContentLength("invalid")).toBeNull();
|
|
});
|
|
|
|
test("streams a body within the limit without buffering it as one payload", async () => {
|
|
const input = Readable.from([Buffer.from("ab"), Buffer.from("cd")]);
|
|
const output = createLimitedRequestBody(input as never, 4);
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of output) chunks.push(Buffer.from(chunk));
|
|
expect(Buffer.concat(chunks).toString("utf8")).toBe("abcd");
|
|
});
|
|
|
|
test("fails as soon as a chunk crosses the configured limit", async () => {
|
|
const input = Readable.from([Buffer.from("abc"), Buffer.from("de")]);
|
|
const output = createLimitedRequestBody(input as never, 4);
|
|
await expect(async () => {
|
|
for await (const chunk of output) {
|
|
// Consume the stream so the limiter observes every chunk.
|
|
void chunk;
|
|
}
|
|
}).rejects.toBeInstanceOf(RequestBodyTooLargeError);
|
|
});
|
|
|
|
test("uses a small default and explicit upload-route exceptions", () => {
|
|
expect(resolveWebRequestBodyLimit("/acme/settings/members")).toBe(DEFAULT_WEB_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/acme/files/upload/up_1/chunk/0")).toBe(5 * 1024 * 1024);
|
|
expect(resolveWebRequestBodyLimit("/acme/projects/project_1/upload", "portal")).toBe(PORTAL_UPLOAD_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/projects/project_1/upload", "portal")).toBe(PORTAL_UPLOAD_REQUEST_BODY_BYTES);
|
|
expect(PORTAL_UPLOAD_REQUEST_BODY_BYTES).toBe(101 * 1024 * 1024);
|
|
expect(PORTAL_UPLOAD_MULTIPART_OVERHEAD_BYTES).toBe(1024 * 1024);
|
|
expect(resolveWebRequestBodyLimit("/acme/projects/project_1/upload", "core")).toBe(DEFAULT_WEB_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/acme/settings/profile")).toBe(8 * 1024 * 1024);
|
|
expect(resolveWebRequestBodyLimit("/settings/profile")).toBe(8 * 1024 * 1024);
|
|
expect(resolveWebRequestBodyLimit("/acme/hr/my-documents")).toBe(26 * 1024 * 1024);
|
|
expect(resolveWebRequestBodyLimit("/acme/emails/new")).toBe(MAX_WEB_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/api/embed/forms/form_1/submit")).toBe(PUBLIC_FORM_REQUEST_BODY_BYTES);
|
|
expect(PUBLIC_FORM_REQUEST_BODY_BYTES).toBe(30 * 1024 * 1024);
|
|
expect(resolveWebRequestBodyLimit("/oauth/register")).toBe(OAUTH_REGISTRATION_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/oauth/token")).toBe(OAUTH_REGISTRATION_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/oauth/revoke")).toBe(OAUTH_REGISTRATION_REQUEST_BODY_BYTES);
|
|
expect(OAUTH_REGISTRATION_REQUEST_BODY_BYTES).toBe(64 * 1024);
|
|
expect(resolveWebRequestBodyLimit("/webhooks/email/google")).toBe(EMAIL_WEBHOOK_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/webhooks/email/microsoft")).toBe(EMAIL_WEBHOOK_REQUEST_BODY_BYTES);
|
|
expect(EMAIL_WEBHOOK_REQUEST_BODY_BYTES).toBe(256 * 1024);
|
|
expect(resolveWebRequestBodyLimit("/register")).toBe(PUBLIC_AUTH_FORM_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/signup")).toBe(PUBLIC_AUTH_FORM_REQUEST_BODY_BYTES);
|
|
expect(PUBLIC_AUTH_FORM_REQUEST_BODY_BYTES).toBe(64 * 1024);
|
|
expect(resolveWebRequestBodyLimit("/b/acme/consulting")).toBe(PUBLIC_BOOKING_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/b/acme/consulting/embed")).toBe(PUBLIC_BOOKING_REQUEST_BODY_BYTES);
|
|
expect(PUBLIC_BOOKING_REQUEST_BODY_BYTES).toBe(64 * 1024);
|
|
expect(resolveWebRequestBodyLimit("/settings/general")).toBe(MAX_WEB_REQUEST_BODY_BYTES);
|
|
});
|
|
|
|
test("does not grant upload limits to lookalike paths", () => {
|
|
expect(resolveWebRequestBodyLimit("/acme/files/upload/up_1/chunk/not-a-number")).toBe(DEFAULT_WEB_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/acme/projects/project_1/upload/extra", "portal")).toBe(DEFAULT_WEB_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/acme/projects/project%2F1/upload", "portal")).toBe(DEFAULT_WEB_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/evil/api/embed/forms/form_1/submit")).toBe(DEFAULT_WEB_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/oauth/register/extra")).toBe(DEFAULT_WEB_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/evil/webhooks/email/microsoft")).toBe(DEFAULT_WEB_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("/evil/b/acme/consulting")).toBe(DEFAULT_WEB_REQUEST_BODY_BYTES);
|
|
expect(resolveWebRequestBodyLimit("https://app.example.test/acme/emails/new/extra")).toBe(DEFAULT_WEB_REQUEST_BODY_BYTES);
|
|
});
|
|
});
|