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:
@@ -0,0 +1,49 @@
|
||||
package blobgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// ErrOutputTooLarge is returned by a reader from LimitReader once it has
|
||||
// been asked for more than its limit. It bounds how far an untrusted
|
||||
// compressed stream may expand, so a small, highly compressible object
|
||||
// from the store cannot decompress without limit.
|
||||
var ErrOutputTooLarge = errors.New("output exceeds size limit")
|
||||
|
||||
// LimitReader returns a reader that yields at most limit bytes from r and
|
||||
// then fails with ErrOutputTooLarge. Unlike io.LimitReader, which reports
|
||||
// a silent io.EOF at the limit (indistinguishable from a stream that
|
||||
// simply ended), this fails, so a caller decoding or copying the stream
|
||||
// sees an error rather than a truncated value. A stream of exactly limit
|
||||
// bytes reads back cleanly to EOF; the first byte beyond it is the error.
|
||||
func LimitReader(r io.Reader, limit int64) io.Reader {
|
||||
// remaining counts down from limit+1: the extra byte is the one that,
|
||||
// if it ever arrives, proves the stream is longer than the limit.
|
||||
return &limitReader{r: r, remaining: limit + 1}
|
||||
}
|
||||
|
||||
type limitReader struct {
|
||||
r io.Reader
|
||||
remaining int64
|
||||
}
|
||||
|
||||
func (l *limitReader) Read(p []byte) (int, error) {
|
||||
if l.remaining <= 0 {
|
||||
return 0, ErrOutputTooLarge
|
||||
}
|
||||
|
||||
if int64(len(p)) > l.remaining {
|
||||
p = p[:l.remaining]
|
||||
}
|
||||
|
||||
n, err := l.r.Read(p)
|
||||
l.remaining -= int64(n)
|
||||
|
||||
if l.remaining <= 0 {
|
||||
// The (limit+1)th byte was just read: the stream is too long.
|
||||
return n, ErrOutputTooLarge
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
Reference in New Issue
Block a user