Originally created by: Ayush7614
Middleware registered via app.use() — including rateLimit(), ipRestriction(), csrf(), and other hooks that attach to beforeHandle — is not invoked when the router returns no match (404) or wrong method (405). Only App({ hooks }) global onRequest and route-level hooks on a matched route run.
This is a defense-in-depth / DoS footgun: attackers can flood random paths and bypass perimeter controls that operators reasonably expect to apply to every request.
This is not an auth bypass on protected handlers — matched routes still run
beforeHandle. It is a perimeter / resource-exhaustion issue.
@daloyjs/core@1.0.0-beta.4 (local monorepo main).1.0.0-beta.4app.request() in-process; also verified over TCP against examples/dast-server.ts)With rateLimit({ windowMs: 60_000, max: 5 }) registered via app.use():
GET /missing-{0..9} → all 404, zero 429GET /ok (the only registered route) → all 429 (limit enforced)Same behavior live against the bookstore example (examples/build-app.ts uses app.use(rateLimit({ max: 120 }))): 150× requests to /nonexistent-* → 150× 404, 0× 429; matched GET /books/1 hits the limit normally.
app.use(rateLimit(...)) (and similar global guards) should run on every inbound request, including 404/405 cold paths — or the framework should document clearly that app.use() guards are match-only and operators must use App({ hooks }) for perimeter controls.
app.use() pushes into groupHooks (src/app.ts ~2915).groupHooks merge into mergedHooks only when router.find() succeeds (~2238).globalHooks.onRequest only (~3324–3430)._globalHooksCache is options.hooks only — explicitly excludes groupHooks on the cold path.import { App, rateLimit } from "@daloyjs/core";
import { z } from "zod";
const app = new App({ env: "test" })
.use(rateLimit({ windowMs: 60_000, max: 5 }))
.route({
method: "GET",
path: "/ok",
responses: { 200: { description: "ok", body: z.object({ ok: z.boolean() }) } },
handler: async () => ({ status: 200 as const, body: { ok: true } }),
});
// All 404 — rate limit never increments
for (let i = 0; i < 10; i++) {
console.log("miss", i, (await app.request(`/missing-${i}`)).status);
}
// All 429 — limit enforced on matched route
for (let i = 0; i < 10; i++) {
console.log("hit", i, (await app.request(`/ok?q=${i}`)).status);
}
Expected output pattern: miss lines all 404; hit lines eventually 429.
Run a lightweight merge of groupHooks.beforeHandle on the 404/405 path (similar to the OPTIONS preflight branch at ~3401–3416), or document + lint (daloy doctor) when perimeter middleware is registered only via app.use().
Medium — DoS / perimeter bypass, not handler auth bypass.
Originally posted by: Ayush7614
cc: @devlinduldulao