From c15fb97706807d9a95837b04c8bfe819ff7d180e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 21:00:46 +0200 Subject: [PATCH] Test: malformed URL must not reach a sibling prefix's not-found handler (GHSA-p68q-wchp-6fh7) Red on fastify 5.8.5, the live lockfile. Co-Authored-By: Claude Sonnet 5.5 --- tests/fastify-malformed-url.test.ts | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/fastify-malformed-url.test.ts diff --git a/tests/fastify-malformed-url.test.ts b/tests/fastify-malformed-url.test.ts new file mode 100644 index 0000000..f515c74 --- /dev/null +++ b/tests/fastify-malformed-url.test.ts @@ -0,0 +1,47 @@ +import Fastify, { type FastifyReply, type FastifyRequest } from "fastify"; +import { afterEach, describe, expect, it } from "vitest"; + +// GHSA-p68q-wchp-6fh7 (fastify < 5.12.2): a malformed request target under one +// plugin prefix, with a method that has no route in the main router, was +// dispatched to the not-found handler of a sibling plugin, skipping that +// handler's preHandler. This pins the framework fix; it is not about a Heimdall +// route. +describe("fastify malformed URL under sibling not-found handlers", () => { + const apps: Array> = []; + afterEach(async () => { + await Promise.all(apps.splice(0).map((app) => app.close())); + }); + + async function build() { + const app = Fastify(); + apps.push(app); + await app.register(async (pub) => { + pub.get("/ping", async () => "pong"); + }, { prefix: "/public" }); + await app.register(async (priv) => { + priv.setNotFoundHandler( + { + preHandler: async (request: FastifyRequest, reply: FastifyReply) => { + if (!request.headers.authorization) return reply.code(401).send({ error: "auth" }); + }, + }, + (_request: FastifyRequest, reply: FastifyReply) => reply.code(404).send({ secret: "PRIVATE-TENANT-DATA" }), + ); + priv.get("/x", async () => "x"); + }, { prefix: "/private" }); + return app; + } + + it("fails closed with 400 and never reaches another prefix's not-found handler", async () => { + const app = await build(); + const response = await app.inject({ method: "DELETE", url: "/public/%zz" }); + expect(response.body).not.toContain("PRIVATE-TENANT-DATA"); + expect(response.statusCode).toBe(400); + }); + + it("still runs the authenticated not-found preHandler for a well-formed URL", async () => { + const app = await build(); + const response = await app.inject({ method: "DELETE", url: "/private/nothing" }); + expect(response.statusCode).toBe(401); + }); +});