diff --git a/package.json b/package.json index 0c6cf4f..36394ef 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@gamecult/cultcache-ts": "file:vendor/CultLib/packages/cultcache-ts", "cultmesh-ts": "file:vendor/CultLib/packages/cultmesh-ts", "cultnet-ts": "file:vendor/CultLib/packages/cultnet-ts", - "fastify": "^5.8.5", + "fastify": "^5.12.6", "pg": "^8.20.0", "zod": "^3.25.76" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 005bb26..855ad01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,8 +28,8 @@ importers: specifier: file:vendor/CultLib/packages/cultnet-ts version: file:vendor/CultLib/packages/cultnet-ts fastify: - specifier: ^5.8.5 - version: 5.8.5 + specifier: ^5.12.6 + version: 5.12.7 pg: specifier: ^8.20.0 version: 8.20.0 @@ -532,17 +532,23 @@ packages: fast-json-stringify@6.3.0: resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-json-stringify@7.1.3: + resolution: {integrity: sha512-5TDM1yOF/BDFOclKz2G6rs1RsIl5HXMCrg/npmdNIPNQCezshR5kIHjhwBNuMY67jhkTUAm9DguL8xkL/x0Wtw==} + fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@4.2.3: + resolution: {integrity: sha512-I1PXwP5eAhlNlcdpjt9CgB59op2ZvZfxw+BkqGFVIUOgNpCVTcTqp3HhTX7brxj5vsip+nSY4nTvff+eIA76hg==} + fastify-plugin@5.1.0: resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} - fastify@5.8.5: - resolution: {integrity: sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==} + fastify@5.12.7: + resolution: {integrity: sha512-v4pEPsTRPuqlDIYiQFB+BHjVN4SdHX9u0ABk0ZDUKbbdY1vsNryUd1j4Z+jQEZajvYIVk047Kimva9/AJ79GYw==} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -556,8 +562,8 @@ packages: picomatch: optional: true - find-my-way@9.5.0: - resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + find-my-way@9.10.0: + resolution: {integrity: sha512-eru+uCPQ2g+nLBkf84MHP/KyNNMar0BDy0j+i8Fb72GF9/YHBZ58q7FODUtfVV7XU9nfRUHXSmaM8NmZcIbZ4Q==} engines: {node: '>=20'} fsevents@2.3.3: @@ -853,8 +859,8 @@ packages: process-warning@4.0.1: resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} - process-warning@5.0.0: - resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process-warning@5.1.1: + resolution: {integrity: sha512-oCkNVj4OKnKEcocVBcDfiIugN1/cE6rgfT3nCtzeqZPA8+rIxMaQ0xXzIkGP8Gj2qUaP1UR7uhJ1VD8lUCpzCg==} quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -1475,15 +1481,26 @@ snapshots: json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 + fast-json-stringify@7.1.3: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 4.2.3 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-querystring@1.1.2: dependencies: fast-decode-uri-component: 1.0.1 fast-uri@3.1.0: {} + fast-uri@4.2.3: {} + fastify-plugin@5.1.0: {} - fastify@5.8.5: + fastify@5.12.7: dependencies: '@fastify/ajv-compiler': 4.0.5 '@fastify/error': 4.2.0 @@ -1491,11 +1508,11 @@ snapshots: '@fastify/proxy-addr': 5.1.0 abstract-logging: 2.0.1 avvio: 9.2.0 - fast-json-stringify: 6.3.0 - find-my-way: 9.5.0 + fast-json-stringify: 7.1.3 + find-my-way: 9.10.0 light-my-request: 6.6.0 pino: 10.3.1 - process-warning: 5.0.0 + process-warning: 5.1.1 rfdc: 1.4.1 secure-json-parse: 4.1.0 semver: 7.7.4 @@ -1509,7 +1526,7 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - find-my-way@9.5.0: + find-my-way@9.10.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 @@ -1733,7 +1750,7 @@ snapshots: on-exit-leak-free: 2.1.2 pino-abstract-transport: 3.0.0 pino-std-serializers: 7.1.0 - process-warning: 5.0.0 + process-warning: 5.1.1 quick-format-unescaped: 4.0.4 real-require: 0.2.0 safe-stable-stringify: 2.5.0 @@ -1758,7 +1775,7 @@ snapshots: process-warning@4.0.1: {} - process-warning@5.0.0: {} + process-warning@5.1.1: {} quick-format-unescaped@4.0.4: {} 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); + }); +});