Found by the round-1 adversarial audit of cli#572 (which hardens the sibling download path). Pre-existing — not introduced by #572.
internal/cmd/generate_output.go has the same renderer-sanitizes-while-its-twin-does-not split that #566 item 2 was about, one command over. #572 gates downloadBlobTo's Saved line (:434, safeTerm(target)) — so the author already treated target as untrusted — while every error string on the same function's failure paths prints it raw.
The text is genuinely server-derived
renderOutName's own doc comment states it: "🔴 BOTH INPUTS ARE UNTRUSTED. The workflow id is SERVER-supplied…". {workflow} expands to filepath.Base(strings.TrimSpace(workflowID)), outputTarget joins that into target, and downloadBlobTo derives name := filepath.Base(target). So the server's workflow id reaches the leaf of both.
cmd/civitai/main.go:57 prints err.Error() unfiltered.
Sites
| line |
string |
carries |
:408 |
refusing to overwrite the existing file %s |
target |
:419 |
download %s: %w |
name |
:433 |
install %s: %w |
target |
:388, :390, :392 |
blobStatusError's three arms |
name |
blobStatusError (:382) is the structural twin of downloadStatusError — same signature, same defer civitai.TagStatus(status, err), same arms. #572 gated that one once at the top (name = safeTerm(name)) with a comment recording that four spellings of one rule drift apart. This twin was not gated.
Deliberately NOT in scope
:413 create output directory %s: %w is correctly ungated, for the same reason #572 removed its own copy of that gate: dir is filepath.Dir(target), which is outputTarget's outDir — user-supplied via --out-dir, never the server's leaf, because outputTarget refuses any name where name != filepath.Base(name). Sanitizing it would strip user-typed bytes, which internal/saferune's rule forbids. Do not "fix" it.
Known residual, stated rather than hidden
saferune deliberately retains \n and \t, so routing these through safeTerm does not close newline line-forgery on this path either. That class is #552's, and is not part of the condition below.
Closing condition
The six sites in the table route their server-derived argument through safeTerm — blobStatusError gated once at the top like its twin, not per-arm — and safeTermCoveredBy["downloadBlobTo"] and ["blobStatusError"] carry rows whose why names them.
Checked by: go test ./internal/cmd -count=1 exits non-zero when safeTerm is removed from blobStatusError, with the failure naming that function rather than a build error.
Found by the round-1 adversarial audit of cli#572 (which hardens the sibling
downloadpath). Pre-existing — not introduced by #572.internal/cmd/generate_output.gohas the same renderer-sanitizes-while-its-twin-does-not split that #566 item 2 was about, one command over. #572 gatesdownloadBlobTo'sSavedline (:434,safeTerm(target)) — so the author already treatedtargetas untrusted — while every error string on the same function's failure paths prints it raw.The text is genuinely server-derived
renderOutName's own doc comment states it: "🔴 BOTH INPUTS ARE UNTRUSTED. The workflow id is SERVER-supplied…".{workflow}expands tofilepath.Base(strings.TrimSpace(workflowID)),outputTargetjoins that intotarget, anddownloadBlobToderivesname := filepath.Base(target). So the server's workflow id reaches the leaf of both.cmd/civitai/main.go:57printserr.Error()unfiltered.Sites
:408refusing to overwrite the existing file %starget:419download %s: %wname:433install %s: %wtarget:388,:390,:392blobStatusError's three armsnameblobStatusError(:382) is the structural twin ofdownloadStatusError— same signature, samedefer civitai.TagStatus(status, err), same arms. #572 gated that one once at the top (name = safeTerm(name)) with a comment recording that four spellings of one rule drift apart. This twin was not gated.Deliberately NOT in scope
:413create output directory %s: %wis correctly ungated, for the same reason #572 removed its own copy of that gate:dirisfilepath.Dir(target), which isoutputTarget'soutDir— user-supplied via--out-dir, never the server's leaf, becauseoutputTargetrefuses anynamewherename != filepath.Base(name). Sanitizing it would strip user-typed bytes, whichinternal/saferune's rule forbids. Do not "fix" it.Known residual, stated rather than hidden
saferunedeliberately retains\nand\t, so routing these throughsafeTermdoes not close newline line-forgery on this path either. That class is #552's, and is not part of the condition below.Closing condition
The six sites in the table route their server-derived argument through
safeTerm—blobStatusErrorgated once at the top like its twin, not per-arm — andsafeTermCoveredBy["downloadBlobTo"]and["blobStatusError"]carry rows whosewhynames them.Checked by:
go test ./internal/cmd -count=1exits non-zero whensafeTermis removed fromblobStatusError, with the failure naming that function rather than a build error.