Bound download expansion and escape control chars on the terminal (closes #164)
Objects fetched from the store are untrusted; several decode paths let one expand or print without limit. - blobgen.LimitReader errors past a byte cap (not io.LimitReader silent EOF). DecodeManifest reads through caps on both compressed input and decompressed output, far above any real manifest, so json.Decode cannot buffer a compressible bomb. FetchAndDecryptBlob bounds decompression to the blob recorded uncompressed_size (not the restoring host blob_size_limit). - downloadSnapshotDB streams straight from storage to its temp file with io.Copy, replacing two ReadAll calls that held the whole database twice. - FetchBlob drops the per-blob Stat round-trip, its expectedSize parameter and returned size, all of which only fed a debug log. - TTYHandler and ui.Writer escape control characters in messages, attribute keys/values, and rendered identifiers/paths before colour codes are applied, so a crafted value cannot drive the terminal. Model: opus-4-8
This commit was merged in pull request #197.
This commit is contained in:
+22
-3
@@ -23,7 +23,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"golang.org/x/term"
|
||||
@@ -225,17 +227,17 @@ func (w *Writer) Hex(s string) string {
|
||||
short = s[:hexAbbrevLen] + "..."
|
||||
}
|
||||
|
||||
return w.paint(ansiCyan, short)
|
||||
return w.paint(ansiCyan, sanitize(short))
|
||||
}
|
||||
|
||||
// Snapshot colorizes a snapshot ID (full, no abbreviation).
|
||||
func (w *Writer) Snapshot(id string) string {
|
||||
return w.paint(ansiCyan+ansiBold, id)
|
||||
return w.paint(ansiCyan+ansiBold, sanitize(id))
|
||||
}
|
||||
|
||||
// Path colorizes a filesystem path.
|
||||
func (w *Writer) Path(p string) string {
|
||||
return w.paint(ansiBlue, p)
|
||||
return w.paint(ansiBlue, sanitize(p))
|
||||
}
|
||||
|
||||
// Size colorizes a byte count using humanize.Bytes.
|
||||
@@ -310,6 +312,23 @@ func (w *Writer) paint(color, s string) string {
|
||||
return color + s + ansiReset
|
||||
}
|
||||
|
||||
// sanitize returns s unchanged when every rune in it is printable, and a
|
||||
// double-quoted, backslash-escaped form (\n, \x1b, …) otherwise. The
|
||||
// string value formatters escape their argument through this before
|
||||
// painting: identifiers, paths and symlink targets they render come from
|
||||
// the snapshot database, which is not trusted, and escaping must happen
|
||||
// before colour is applied — the painted result already contains the
|
||||
// escape codes the raw text would otherwise be indistinguishable from.
|
||||
func sanitize(s string) string {
|
||||
for _, r := range s {
|
||||
if !unicode.IsPrint(r) {
|
||||
return strconv.Quote(s)
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// emit writes "<prefix> <body>\n" with the prefix painted in prefixColor
|
||||
// and the body optionally painted in bodyColor (empty = no body color).
|
||||
func (w *Writer) emit(prefixColor, prefix, bodyColor, format string, args []any) {
|
||||
|
||||
Reference in New Issue
Block a user