Skip to content

samples/restApi: pre-parse templates (gosec G708 hardening)#124

Open
randomizedcoder wants to merge 1 commit into
NVIDIA:mainfrom
randomizedcoder:fix/g708-template-injection
Open

samples/restApi: pre-parse templates (gosec G708 hardening)#124
randomizedcoder wants to merge 1 commit into
NVIDIA:mainfrom
randomizedcoder:fix/g708-template-injection

Conversation

@randomizedcoder

Copy link
Copy Markdown

Hi 👋

We were running some static analysis over a fork of this repo (golangci-lint + gosec, tier-1 config) and gosec flagged a couple of G708 (server-side template injection) HIGH-severity findings in samples/restApi/handlers/utils.go:

samples/restApi/handlers/utils.go:179  G708 (CWE-94)  printer()
samples/restApi/handlers/utils.go:199  G708 (CWE-94)  processPrint()

The flagged statements parse a text/template from a value that gosec's taint analysis treats as attacker-reachable:

func printer(resp http.ResponseWriter, req *http.Request, stats any, templ string) {
    t := template.Must(template.New("").Parse(templ))  // <-- G708
    ...
}

Today every caller passes a package-level const string, so this is fragile-by-structure rather than unsafe-today. But sample code tends to get copy-pasted into production, and the next reader who wires HTTP form input into templ gets RCE. The fix is purely structural.

What this PR does

  • Pre-parses every template once at package init into a package-level *template.Template (deviceInfoTmpl, processInfoTmpl, etc.).
  • Tightens printer's signature from templ string to tmpl *template.Template. The function can no longer accept a free-form string, so the taint source is closed at the type-system level — the compiler will reject any future printer(..., userInput) call.
  • Same treatment for processPrint (now uses processInfoTmpl).
  • Updates the 7 printer call sites in byIds.go and byUuids.go to pass the pre-parsed templates.

Templates stay text/template (the samples render plain-text curl-style output, not HTML) — no escaping behavior changes.

TDD + table-driven tests

Added samples/restApi/handlers/utils_test.go (18 cases across two table-driven tests) covering five categories per the test struct's category tag:

Category What it asserts
positive Known-good stats render expected substrings
negative Templates that can't render the stats produce a logged error
boundary Zero/empty stats, single-element collections, no panic
corner Unicode, oversized strings
attacker {{ ... }} template syntax embedded in stats data renders as data, NOT re-evaluated as a template — this is the core security assertion that the pre-parsed template doesn't re-parse the payload

Tests are GPU-less (httptest.NewRecorder only, no dcgm.Init), so they run on any CI runner. House style follows pkg/dcgm/diag_test.go: stretchr/testify/assert + t.Run(tt.name, ...) subtests.

Why this is low risk

  • No public API change (all touched identifiers are package-private).
  • No behavior change for any current caller — every existing call site was already passing a constant.
  • All 9 sample binaries build cleanly (go build ./samples/...).
  • go vet, gofmt, and the new go test ./samples/restApi/handlers/... all green.
  • Drive-by: DevicesUuids() loop modernized from for i := uint(0); i < count; i++ to for i := range count (Go 1.22+ rangeint, since go.mod is go 1.23). Happy to split this out if you'd prefer a single-concern PR.

Cheers, and thanks for go-dcgm — it's been a great library to work with.

Test plan

  • go build ./samples/restApi/...
  • go test -v -count=1 ./samples/restApi/handlers/... — 18/18 pass
  • go vet ./samples/restApi/...
  • gofmt -l samples/restApi/handlers/ — empty
  • CI green

🤖 Generated with Claude Code

…ction

printer() previously accepted `templ string` and parsed it inline, which
gosec correctly flags as a server-side template injection (G708): if any
future caller ever wires HTTP input into templ, that's RCE.

Refactor so every template is parsed once at package init into a
package-level *template.Template, and tighten printer's signature to
accept *template.Template. The function can no longer accept a free-form
string, so the taint source is closed at the type level. Same pattern
for processPrint + processInfoTmpl.

Adds table-driven tests under samples/restApi/handlers/utils_test.go
covering positive, negative, boundary, corner, and attacker categories.
The attacker cases verify that values containing {{ ... }} in stats
fields are rendered as data, NOT re-interpreted as template directives.

Drive-by: modernize DevicesUuids loop from `for i := uint(0); i < count; i++`
to `for i := range count` (Go 1.22+ rangeint).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: randomizedcoder <dave.seddon.ca@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant