package rules import ( "go/ast" "strings" ) // web/httputil v1.6.0 added Bind/BindEmpty, which fill a typed Req from path and // query struct tags and validate it once. Reading a parameter by hand — chi.URLParam // or r.URL.Query() inside a handler — skips that validation, which is the path by // which unclamped bounds and 500s-that-should-be-400s reach production. This rule // nudges toward Bind; it is advisory (info), since a genuinely custom response // (streaming, non-JSON) may still read parameters directly. func init() { registered = append(registered, Rule{ ID: "httputil.prefer-bind", Severity: SeverityInfo, Module: "web", Check: checkPreferBind, }, ) } func checkPreferBind(c *Context) []Finding { const ( msg = "reading a path/query parameter by hand skips validation — httputil.Bind / BindEmpty (v1.6.0) fill a typed Req from path:/query: tags and validate it once" hint = "Declare the parameter as a struct field (path:\"id\" / query:\"page\") and use httputil.Bind. Keep manual parsing only for genuinely custom responses (streaming, non-JSON)." ) var hits []Finding seen := map[int]bool{} ast.Inspect(c.File, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } name := exprName(call.Fun) manual := strings.HasSuffix(name, ".URLParam") || strings.HasSuffix(name, ".URLParamFromCtx") || strings.HasSuffix(name, ".URL.Query") if !manual { return true } line := c.Fset.Position(call.Pos()).Line if seen[line] { return true } seen[line] = true hits = append(hits, Finding{Message: msg, Hint: hint, Line: line}) return true }) return hits }