package rules import ( "go/ast" "go/token" "strings" ) // The web/mw CORS convention: mw.CORS uses exact-origin matching and rejects "*" // at construction (panic). Passing "*" is the trap this rule catches before runtime — // allow-all is done with mw.CORSAllowAll(), gated by env in application code. func init() { registered = append(registered, Rule{ ID: "cors.wildcard-noop", Severity: SeverityError, Module: "web", Check: checkCORSWildcard, }, Rule{ ID: "web.allowedorigins-removed", Severity: SeverityError, Module: "web", Check: checkAllowedOriginsRemoved, }, ) } // checkAllowedOriginsRemoved flags any reference to the removed // web.Config.AllowedOrigins field — both a selector (cfg.Web.AllowedOrigins) and a // struct-literal key (web.Config{AllowedOrigins: ...}). It was env-backed through // v1.1.x, became a code-only override in v1.2.0, and was removed in v1.3.0. Code // that still reads it compiled but silently served no CORS in v1.2.0; in v1.3.0 it // no longer compiles. CORS now lives solely on server.Config.CORSOrigins. func checkAllowedOriginsRemoved(c *Context) []Finding { const ( msg = "web.Config.AllowedOrigins was removed in v1.3.0 — CORS lives on Server.CORSOrigins (env EINHERJAR_SERVER_CORS_ORIGINS)" hint = "Read cfg.Server.CORSOrigins (or set it in code); web.New applies it automatically. Never reintroduce a field/var for CORS origins." ) seen := map[int]bool{} var hits []Finding add := func(pos token.Pos) { line := c.Fset.Position(pos).Line if seen[line] { return } seen[line] = true hits = append(hits, Finding{Message: msg, Hint: hint, Line: line}) } ast.Inspect(c.File, func(n ast.Node) bool { switch e := n.(type) { case *ast.SelectorExpr: if e.Sel != nil && e.Sel.Name == "AllowedOrigins" { add(e.Sel.Pos()) } case *ast.KeyValueExpr: if id, ok := e.Key.(*ast.Ident); ok && id.Name == "AllowedOrigins" { add(id.Pos()) } } return true }) return hits } // checkCORSWildcard flags any call to .CORS(...) whose arguments contain a // "*" string literal — which mw.CORS rejects (panics) at boot. func checkCORSWildcard(c *Context) []Finding { var hits []Finding ast.Inspect(c.File, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok || !strings.HasSuffix(exprName(call.Fun), ".CORS") { return true } for _, arg := range call.Args { ast.Inspect(arg, func(m ast.Node) bool { lit, ok := m.(*ast.BasicLit) if ok && lit.Kind == token.STRING && strings.Trim(lit.Value, `"`) == "*" { hits = append(hits, Finding{ Message: `mw.CORS with "*" panics at construction — "*" matches no real origin (exact-match only)`, Hint: "Use mw.CORSAllowAll() for local development, or list explicit origins.", Line: c.Fset.Position(call.Pos()).Line, }) } return true }) } return true }) return hits }