fix: enforce API request limits
Add a single platform-owned request-body policy (hono/body-limit) with small, named limits for Better Auth and Core's grant-mutation endpoint, and a new required maxBodyBytes field on the executable module route contract so every module route makes a conscious no-body-or-bounded-body choice, capped at a platform ceiling. Every oversized request gets one sanitized 413 contract and never reaches business logic, authorization, or a handler. Also configure finite Node header/request/keep-alive timeouts, including the non-obvious connectionsCheckingInterval needed for them to take effect promptly, so a slow-header or slow-body connection cannot hold a socket indefinitely. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cafd513019
commit
d3c6bd73d3
@@ -444,6 +444,7 @@ describe("module HTTP routes", () => {
|
||||
method: "GET",
|
||||
path: "/items",
|
||||
permission: "single.items.read",
|
||||
maxBodyBytes: 1024,
|
||||
handler: ${noopHandlerSource()},
|
||||
});
|
||||
}
|
||||
@@ -473,8 +474,8 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "alpha", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/first", permission: "alpha.first.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/second", permission: "alpha.second.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/first", permission: "alpha.first.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/second", permission: "alpha.second.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -485,7 +486,7 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "beta", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/third", permission: "beta.third.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/third", permission: "beta.third.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -509,8 +510,8 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "same-path", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/items", permission: "same-path.items.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "POST", path: "/items", permission: "same-path.items.write", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/items", permission: "same-path.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "POST", path: "/items", permission: "same-path.items.write", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -532,8 +533,8 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "dup", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/items", permission: "dup.items.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/items", permission: "dup.items.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/items", permission: "dup.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/items", permission: "dup.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -551,8 +552,8 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "param-shape", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/events/:eventId", permission: "param-shape.events.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/events/:id", permission: "param-shape.events.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/events/:eventId", permission: "param-shape.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/events/:id", permission: "param-shape.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -570,7 +571,7 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "bad-method", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "get", path: "/items", permission: "bad-method.items.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "get", path: "/items", permission: "bad-method.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -586,7 +587,7 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "bad-path", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "items", permission: "bad-path.items.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "items", permission: "bad-path.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -594,6 +595,22 @@ describe("module HTTP routes", () => {
|
||||
"MODULE_ROUTE_PATH_INVALID",
|
||||
]);
|
||||
|
||||
const badMaxBodyBytesDirectory = await createModulesDirectory();
|
||||
await createRuntimeModule(badMaxBodyBytesDirectory, "bad-max-body-bytes", {
|
||||
manifest: manifestFor("bad-max-body-bytes", {
|
||||
permissions: [permissionDeclaration("bad-max-body-bytes.items.read")],
|
||||
}),
|
||||
serverSource: `export default {
|
||||
id: "bad-max-body-bytes", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/items", permission: "bad-max-body-bytes.items.read", maxBodyBytes: -1, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
expect(runtimeIssueCodes(await captureRuntimeError(badMaxBodyBytesDirectory))).toEqual([
|
||||
"MODULE_ROUTE_MAX_BODY_BYTES_INVALID",
|
||||
]);
|
||||
|
||||
const badDefinitionDirectory = await createModulesDirectory();
|
||||
await createRuntimeModule(badDefinitionDirectory, "bad-shape", {
|
||||
manifest: manifestFor("bad-shape", {
|
||||
@@ -624,7 +641,7 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "undeclared", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/items", permission: "undeclared.items.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/items", permission: "undeclared.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -644,6 +661,7 @@ describe("module HTTP routes", () => {
|
||||
method: "GET",
|
||||
path: "/items",
|
||||
permission: "core.authorization.grants.manage",
|
||||
maxBodyBytes: 1024,
|
||||
handler: ${noopHandlerSource()},
|
||||
});
|
||||
}
|
||||
@@ -667,7 +685,7 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "beta", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/items", permission: "alpha.items.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/items", permission: "alpha.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -689,9 +707,9 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "baselines", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/public", permission: "baselines.public.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/authenticated", permission: "baselines.authenticated.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/restricted", permission: "baselines.restricted.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/public", permission: "baselines.public.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/authenticated", permission: "baselines.authenticated.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/restricted", permission: "baselines.restricted.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -701,6 +719,7 @@ describe("module HTTP routes", () => {
|
||||
for (const route of runtime.routes) {
|
||||
expect(Object.keys(route).sort()).toEqual([
|
||||
"handler",
|
||||
"maxBodyBytes",
|
||||
"method",
|
||||
"moduleId",
|
||||
"path",
|
||||
@@ -724,6 +743,7 @@ describe("module HTTP routes", () => {
|
||||
method: "GET",
|
||||
path: "/items",
|
||||
permission: "never-invoked.items.read",
|
||||
maxBodyBytes: 1024,
|
||||
handler: () => { ${recorder.expression}.push("handler:called"); return new Response(null, { status: 204 }); },
|
||||
});
|
||||
}
|
||||
@@ -745,7 +765,7 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "frozen", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/items", permission: "frozen.items.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/items", permission: "frozen.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -772,6 +792,7 @@ describe("module HTTP routes", () => {
|
||||
method: "GET",
|
||||
path: "/items",
|
||||
permission: "mutate.items.read",
|
||||
maxBodyBytes: 1024,
|
||||
handler: ${noopHandlerSource()},
|
||||
};
|
||||
http.route(definition);
|
||||
@@ -800,7 +821,7 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "first-ok", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/items", permission: "first-ok.items.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/items", permission: "first-ok.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -809,7 +830,7 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "second-bad", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/items", permission: "second-bad.items.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/items", permission: "second-bad.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
}
|
||||
};`,
|
||||
});
|
||||
@@ -829,7 +850,7 @@ describe("module HTTP routes", () => {
|
||||
serverSource: `export default {
|
||||
id: "routed", version: "0.1.0",
|
||||
register({ http }) {
|
||||
http.route({ method: "GET", path: "/items", permission: "routed.items.read", handler: ${noopHandlerSource()} });
|
||||
http.route({ method: "GET", path: "/items", permission: "routed.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} });
|
||||
},
|
||||
start() { ${recorder.expression}.push("routed:start"); },
|
||||
stop() { ${recorder.expression}.push("routed:stop"); }
|
||||
|
||||
Reference in New Issue
Block a user