Skip to content

Commit f5a146b

Browse files
committed
fix(webapp): cap the agent's body on any method and on a mixed-case path
1 parent 85b98ab commit f5a146b

2 files changed

Lines changed: 31 additions & 3 deletions

File tree

apps/webapp/app/services/dashboardAgentBodyCap.server.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export const DASHBOARD_AGENT_MAX_INGRESS_BYTES = MAX_MESSAGE_BODY_BYTES + INGRES
1818

1919
const AGENT_PATH = /\/dashboard-agent(\/|$)/;
2020

21+
/** Methods that can carry one. GET and HEAD cannot, and streaming them would be wasted work. */
22+
const BODY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
23+
2124
function refuse(res: Response): void {
2225
if (res.headersSent) return;
2326
res.status(413).json({ error: MESSAGE_TOO_LARGE_ERROR, code: MESSAGE_TOO_LARGE_CODE });
@@ -51,9 +54,12 @@ export function capRequestBody(req: Request, res: Response, limit: number): void
5154
req.once("end", () => req.off("data", onData));
5255
}
5356

54-
/** Only the agent's own paths: every other route keeps the body handling it had. */
57+
/**
58+
* Only the agent's own paths: every other route keeps the body handling it had. Matched
59+
* case-insensitively because Remix routes are, and on every method — a DELETE reads a body too.
60+
*/
5561
export function dashboardAgentBodyCap(req: Request, res: Response, next: NextFunction): void {
56-
if (req.method !== "POST" || !AGENT_PATH.test(req.path)) {
62+
if (!BODY_METHODS.has(req.method) || !AGENT_PATH.test(req.path.toLowerCase())) {
5763
next();
5864
return;
5965
}

apps/webapp/test/dashboardAgentBodyCap.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ async function listen(): Promise<{ url: string; buffered: () => number }> {
1818
let buffered = 0;
1919
const app = express();
2020
app.use(dashboardAgentBodyCap);
21-
app.post("*", async (req, res) => {
21+
app.all("*", async (req, res) => {
2222
try {
2323
for await (const chunk of req) buffered += (chunk as Buffer).byteLength;
2424
} catch {
@@ -114,6 +114,28 @@ describe("the dashboard agent's ingress cap", () => {
114114
expect(await response.json()).toEqual({ bytes: size });
115115
});
116116

117+
it("caps a mixed-case path, because Remix matches routes case-insensitively", async () => {
118+
const { url } = await listen();
119+
120+
const response = await postChunked(
121+
`${url}/api/v1/Dashboard-Agent/watches/batch-check`,
122+
DASHBOARD_AGENT_MAX_INGRESS_BYTES * 4
123+
);
124+
125+
expect(response.status).toBe(413);
126+
});
127+
128+
it("caps a DELETE, which reads a body on the alerts route", async () => {
129+
const { url } = await listen();
130+
131+
const response = await fetch(`${url}/api/v1/dashboard-agent/alerts/ch_1`, {
132+
method: "DELETE",
133+
body: "x".repeat(DASHBOARD_AGENT_MAX_INGRESS_BYTES + 1024),
134+
});
135+
136+
expect(response.status).toBe(413);
137+
});
138+
117139
it("leaves every other path alone", async () => {
118140
const { url, buffered } = await listen();
119141
const size = DASHBOARD_AGENT_MAX_INGRESS_BYTES + 1024;

0 commit comments

Comments
 (0)