2026-08-08 00:55:39 -06:00
|
|
|
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,
|
|
|
|
|
},
|
2026-08-08 10:51:38 -06:00
|
|
|
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 v2.0.0. Code
|
|
|
|
|
// that still reads it compiled but silently served no CORS in v1.2.0; in v2.0.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 v2.0.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."
|
2026-08-08 00:55:39 -06:00
|
|
|
)
|
2026-08-08 10:51:38 -06:00
|
|
|
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
|
2026-08-08 00:55:39 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// checkCORSWildcard flags any call to <pkg>.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
|
|
|
|
|
}
|