Description of the false positive
js/missing-rate-limiting flags route handlers that are, in fact, rate-limited by a global @fastify/rate-limit registration ({ global: true } — the plugin's documented default, which installs an onRequest limiter for every route on the server).
@fastify/rate-limit is modeled (the FastifyRateLimiter class in MissingRateLimiting.qll), and for a plain fastify() server CodeQL correctly recognizes the global limiter as guarding the routes. But when the server instance is created via .withTypeProvider() — the standard Fastify type-provider pattern (e.g. @fastify/type-provider-typebox) — the guard linkage is lost, and every expensive/authorizing route handler is flagged even though the global limiter covers it at runtime.
Because .withTypeProvider() is used by essentially every Fastify + TypeBox project, this affects a broad class of real-world apps that use global rate limiting.
Evidence (CodeQL CLI, bundle 2.26.3 — latest at time of writing)
Controlled experiment building a CodeQL database from a real TypeScript Fastify app (routes defined as TypeBox-typed plugin modules registered under prefixes; a single global @fastify/rate-limit), running only js/missing-rate-limiting:
| App construction |
Global limiter |
Result on the route handlers |
Fastify(opts).withTypeProvider<TypeBoxTypeProvider>() |
present |
flagged (false positive) |
Fastify(opts) — no .withTypeProvider() |
present |
not flagged (correct) |
Fastify(opts) — no .withTypeProvider() |
removed |
flagged (correct — genuinely unguarded) |
Rows 2 and 3 confirm the handlers are recognized and the guard is normally detected. Row 1 vs row 2 isolates .withTypeProvider() as the only change that turns correct detection into a false positive.
Code sample (framework-generic; the affected app is closed-source)
import Fastify from 'fastify'
import rateLimit from '@fastify/rate-limit'
import type { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import v1Routes from './versions/v1' // registers TypeBox-typed route plugins under nested scopes
const app = Fastify(opts).withTypeProvider<TypeBoxTypeProvider>()
await app.register(rateLimit, { global: true, max: 500, timeWindow: '1 minute' })
await app.register(v1Routes, { prefix: '/api/v1' })
// → handlers inside v1Routes are flagged js/missing-rate-limiting,
// although the global limiter above guards them at runtime.
// Removing `.withTypeProvider()` (only) clears the false positives.
Likely area
The guard relationship between FastifyRateLimiter.getRoutingNode() and the route handlers when the server node is a .withTypeProvider() method-call result rather than the raw StandardServerDefinition — see javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll and .../security/dataflow/MissingRateLimiting.qll. Isolated minimal apps (including .withTypeProvider() + nested register() + typed plugin modules) are recognized correctly; reproducing the FP requires .withTypeProvider() plus a composed multi-plugin app, which points at how the type-provider'd server instance maps into the routing model.
Versions: @fastify/rate-limit 10.3.0, fastify 5.x, @fastify/type-provider-typebox; CodeQL bundle 2.26.3.
Description of the false positive
js/missing-rate-limitingflags route handlers that are, in fact, rate-limited by a global@fastify/rate-limitregistration ({ global: true }— the plugin's documented default, which installs anonRequestlimiter for every route on the server).@fastify/rate-limitis modeled (theFastifyRateLimiterclass inMissingRateLimiting.qll), and for a plainfastify()server CodeQL correctly recognizes the global limiter as guarding the routes. But when the server instance is created via.withTypeProvider()— the standard Fastify type-provider pattern (e.g.@fastify/type-provider-typebox) — the guard linkage is lost, and every expensive/authorizing route handler is flagged even though the global limiter covers it at runtime.Because
.withTypeProvider()is used by essentially every Fastify + TypeBox project, this affects a broad class of real-world apps that use global rate limiting.Evidence (CodeQL CLI, bundle 2.26.3 — latest at time of writing)
Controlled experiment building a CodeQL database from a real TypeScript Fastify app (routes defined as TypeBox-typed plugin modules registered under prefixes; a single global
@fastify/rate-limit), running onlyjs/missing-rate-limiting:Fastify(opts).withTypeProvider<TypeBoxTypeProvider>()Fastify(opts)— no.withTypeProvider()Fastify(opts)— no.withTypeProvider()Rows 2 and 3 confirm the handlers are recognized and the guard is normally detected. Row 1 vs row 2 isolates
.withTypeProvider()as the only change that turns correct detection into a false positive.Code sample (framework-generic; the affected app is closed-source)
Likely area
The guard relationship between
FastifyRateLimiter.getRoutingNode()and the route handlers when the server node is a.withTypeProvider()method-call result rather than the rawStandardServerDefinition— seejavascript/ql/lib/semmle/javascript/frameworks/Fastify.qlland.../security/dataflow/MissingRateLimiting.qll. Isolated minimal apps (including.withTypeProvider()+ nestedregister()+ typed plugin modules) are recognized correctly; reproducing the FP requires.withTypeProvider()plus a composed multi-plugin app, which points at how the type-provider'd server instance maps into the routing model.Versions:
@fastify/rate-limit10.3.0,fastify5.x,@fastify/type-provider-typebox; CodeQL bundle 2.26.3.