Skip to content

Newline in a server-supplied file name still forges lines on four download surfaces, and #552's closure left it unowned #577

Description

@ZacxDev

Split out of #552, which was closed by #573 (2026-09-12T17:30:31Z). #573's measured scope was ~13 tabwriter renderers and it deliberately excluded free-text surfaces — its own message says those "deliberately keep safeTerm + indentContinuation — they are not cells." internal/cmd/download.go is not on #552's table and was never covered by it.

So the residual below is real, documented in the tree, and as of #573 had no open tracking object. Found by the round-2 audit of #572.

The residual

internal/saferune deliberately retains \n and \t (Cc, minus \n and \t). safeTerm therefore does not stop a server-supplied files[].name containing a newline from forging whole lines on four surfaces that #572 otherwise hardened:

site surface
download.go (*progressWriter).line the \r-rewritten progress line, re-emitted at 10 Hz on a TTY
download.go checkTargetCollisions the same-target overwrite refusal — forged - [id N] rows inside the group listing
download.go downloadOne the SHA256-mismatch message, i.e. the CLI asserting an integrity failure
download.go downloadStatusError the 401/403/404 arms

Measured with files[].name = "weights.safetensors\nSaved /home/u/legit.safetensors (4.0 GiB) (SHA256 verified)": the progress line renders the forged Saved … (SHA256 verified) before any transfer completes, and done() leaves it standing.

Not a regression. Every one of these surfaces was fully raw before #572; that PR is a strict improvement and states this residual rather than hiding it. What was missing is an owner.

Why it is not simply "apply safeTermSingle"

These are free-text lines, not tabwriter cells, so #573's cell rule does not apply to them unmodified: collapsing \n to a space in a \r-rewritten progress line and in a multi-line refusal has different display consequences than it does in a cell, and the refusal is a fail-safe whose group structure must stay readable. Deciding which transform each surface takes is the work here — this issue does not prescribe one.

Correction to this issue's first draft

The original closing condition below said the four in-tree statements were "three comment blocks in download.go and the README Troubleshooting row". That was wrong, and #572's round-2 fix pass measured it: the README row carries no issue pointer at all. The fourth statement is the header paragraph of internal/cmd/download_safeterm_test.go. Left visible rather than silently edited, since the miscount would have let the condition read as satisfied while a statement still pointed at a closed issue.

Those four are already repointed at this issue by #572, so that half of the condition is discharged when #572 merges.

Closing condition

The four surfaces in the table above render a server-supplied name containing \n (and \t) as a single terminal line, and a test drives each one with a newline-bearing name and asserts the rendered output occupies the expected number of lines — failing if a forged line appears.

Checked by: go test ./internal/cmd -count=1 exits non-zero when the newline transform is removed from (*progressWriter).line, naming that function rather than a build error.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions