Documentation
¶
Overview ¶
Package reqparamlimit catches unclamped request-sourced integers flowing into limit/cap-shaped call parameters. A client-controlled limit is a denial-of-service lever: a request that says "limit: 9_999_999" makes the server size a search, query, or allocation to the attacker's number. Real instance: the MCP docs-search tool handler forwarded params["limit"] (type-switched out of the request map) verbatim into docs.SearchWithLimit, whose hit list then scaled with whatever the client sent (framework/mcp_introspection.go:282, the seed of this analyzer).
Lane: vettool (type-aware), NOT the contracts pattern lane. Two judgements need types.Info and defeat string-pattern rules:
- the callee's parameter NAME at the argument position, resolved from the callee's types.Signature — docs.SearchWithLimit's second parameter is named "limit" whatever the import alias or selector spelling at the call site;
- map type identity: only map[string]any-shaped indexes (string key, empty-interface element — the decoded-JSON params shape, named aliases included) count as request-sourced. Typed config maps (map[string]int) and struct fields never produce an extraction and stay silent.
Sanctioned postures that stay silent:
- a clamp between extraction and use: any comparison of the extracted variable against a constant literal (limit <= 0, limit > 100) or a max*-prefixed identifier or field (limit > maxHits, limit > c.maxHits) in the straight line — this covers both reassignment clamps (limit = maxHits) and reject-guards (if limit > maxHits { return err }).
- an expression clamp: limit := min(params["limit"].(int), 100) — a builtin min/max call anywhere in the assigning expression counts as clamped in place.
- clean reassignment: limit = defaultLimit after extraction clears the taint, since the RHS carries none.
- limits sourced from constants, config structs, or typed maps.
- extraction feeding parameters whose name is outside the set (a string term, a bool flag) — only limit-shaped parameter names are sinks.
Heuristics, exactly as implemented:
- extraction: a type assertion or type switch on m[K] where m is map[string]any-shaped and K is a string literal matching (?i)^(limit|max|hits|count|page_?size|batch_?size|top|take)$. The value may then flow through conversions and plain assignments; flow is local, forward, straight-line.
- use: the tainted value appears in a positional argument whose callee parameter name matches the same set. Callee signatures resolve through types (function or method object, then the static type of the callee expression); signatures whose parameters are unnamed match nothing. When no signature is available at all, fall back to a call-site positional heuristic — only the FINAL positional argument is treated as limit-shaped — and builtins and conversions are excluded outright. The fallback is stated here for completeness: in compiling code the signature almost always resolves, so the fallback is effectively unreachable.
- clamp: a comparison (==, !=, <, <=, >, >=) of a tainted variable against a constant literal or a max* identifier or field, appearing in an if/switch/for condition between extraction and use, clears that variable. Taint learned inside a conditional branch, loop body, or closure stays inside it (closures are checked as fresh functions with no inherited taint); assignments made inside type-switch clauses DO escape the switch, because the switch statement itself always executes.
- one diagnostic per call site: the first matching argument is reported at the argument's position.
Index ¶
Constants ¶
const Doc = "report request-sourced limit-shaped map values passed to limit-shaped parameters without a clamp"
Variables ¶
var Analyzer = &analysis.Analyzer{ Name: "reqparamlimit", Doc: Doc, Run: run, }
Functions ¶
This section is empty.
Types ¶
This section is empty.