package main import ( "fmt" "go/ast" "go/parser" "go/token" "os" "path/filepath" "slices" "strings" ) type errLeakViolation struct { file string line int snippet string } // checkErrLeak finds places where err.Error() is sent directly to HTTP clients. // These leak internal details. Use writeInternalServerError/writeServerError instead. func checkErrLeak(root string) []errLeakViolation { var violations []errLeakViolation if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") { return nil } if strings.Contains(path, "/vendor/") || strings.HasSuffix(path, "_test.go") { return nil } fset := token.NewFileSet() f, err := parser.ParseFile(fset, path, nil, 0) if err != nil { return nil } relPath, _ := filepath.Rel(root, path) ast.Inspect(f, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } // Pattern 1: http.Error(w, err.Error(), ...) if sel, ok := call.Fun.(*ast.SelectorExpr); ok { if ident, ok := sel.X.(*ast.Ident); ok && ident.Name == "http" && sel.Sel.Name == "Error" { if len(call.Args) >= 2 && containsErrorCall(call.Args[1]) { pos := fset.Position(call.Pos()) violations = append(violations, errLeakViolation{ file: relPath, line: pos.Line, snippet: "http.Error(w, err.Error(), ...) \u2014 leaks internal error", }) } } } // Pattern 2: fmt.Fprintf(w, ... err.Error() ...) if sel, ok := call.Fun.(*ast.SelectorExpr); ok { if ident, ok := sel.X.(*ast.Ident); ok && ident.Name == "fmt" { if sel.Sel.Name == "Fprintf" || sel.Sel.Name == "Fprintln" { if len(call.Args) > 1 { if slices.ContainsFunc(call.Args[1:], containsErrorCall) { pos := fset.Position(call.Pos()) violations = append(violations, errLeakViolation{ file: relPath, line: pos.Line, snippet: fmt.Sprintf("fmt.%s(w, ...err.Error()...) \u2014 leaks internal error", sel.Sel.Name), }) } } } } } // Pattern 3: w.Write([]byte(err.Error())) if sel, ok := call.Fun.(*ast.SelectorExpr); ok { if sel.Sel.Name == "Write" && len(call.Args) == 1 { if containsErrorCall(call.Args[0]) { pos := fset.Position(call.Pos()) violations = append(violations, errLeakViolation{ file: relPath, line: pos.Line, snippet: "w.Write([]byte(err.Error())) \u2014 leaks internal error", }) } } } return true }) return nil }); err != nil { violations = append(violations, errLeakViolation{ file: filepath.ToSlash(root), line: 0, snippet: err.Error(), }) } return violations } // containsErrorCall checks if an expression contains a call to .Error() on any receiver identifier. func containsErrorCall(expr ast.Expr) bool { found := false ast.Inspect(expr, func(n ast.Node) bool { if found { return false } call, ok := n.(*ast.CallExpr) if !ok { return true } sel, ok := call.Fun.(*ast.SelectorExpr) if !ok || sel.Sel.Name != "Error" { return true } if _, ok := sel.X.(*ast.Ident); ok { found = true } return true }) return found }