internal/pkg: optionally validate flat pathnames
All checks were successful
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m34s
Test / Hakurei (push) Successful in 3m35s
Test / ShareFS (push) Successful in 3m37s
Test / Hpkg (push) Successful in 4m21s
Test / Sandbox (race detector) (push) Successful in 4m57s
Test / Hakurei (race detector) (push) Successful in 5m50s
Test / Flake checks (push) Successful in 1m42s

This makes the decoder safe against untrusted input without hurting performance for a trusted stream. This should still not be called against untrusted input though.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2026-01-03 18:59:18 +09:00
parent ce249d23f1
commit 55465c6e72
2 changed files with 19 additions and 4 deletions

View File

@@ -8,6 +8,7 @@ import (
"io/fs"
"math"
"os"
"path/filepath"
"syscall"
"hakurei.app/container/check"
@@ -52,8 +53,12 @@ func (ent *FlatEntry) Encode(w io.Writer) (n int, err error) {
return w.Write(payload)
}
// ErrInsecurePath is returned by [FlatEntry.Decode] if validation is requested
// and a nonlocal path is encountered in the stream.
var ErrInsecurePath = errors.New("insecure file path")
// Decode decodes the entry from its representation produced by Encode.
func (ent *FlatEntry) Decode(r io.Reader) (n int, err error) {
func (ent *FlatEntry) Decode(r io.Reader, validate bool) (n int, err error) {
var nr int
header := make([]byte, wordSize*2)
@@ -92,6 +97,11 @@ func (ent *FlatEntry) Decode(r io.Reader) (n int, err error) {
} else {
ent.Data = buf[pPathSize : pPathSize+dataSize]
}
if validate && !filepath.IsLocal(ent.Path) {
err = ErrInsecurePath
}
return
}
@@ -108,11 +118,16 @@ type DirScanner struct {
// Entry to store results in. Its address is returned by the Entry method
// and is updated on every call to Scan.
ent FlatEntry
// Validate pathnames during decoding.
validate bool
}
// NewDirScanner returns the address of a new instance of [DirScanner] reading
// from r. The caller must no longer read from r after this function returns.
func NewDirScanner(r io.Reader) *DirScanner { return &DirScanner{r: r} }
func NewDirScanner(r io.Reader, validate bool) *DirScanner {
return &DirScanner{r: r, validate: validate}
}
// Err returns the first non-EOF I/O error.
func (s *DirScanner) Err() error {
@@ -132,7 +147,7 @@ func (s *DirScanner) Scan() bool {
}
var n int
n, s.err = s.ent.Decode(s.r)
n, s.err = s.ent.Decode(s.r, s.validate)
if errors.Is(s.err, io.EOF) {
return n != 0
}

View File

@@ -85,7 +85,7 @@ func TestFlatten(t *testing.T) {
t.Fatalf("Flatten: error = %v", err)
}
s := pkg.NewDirScanner(bytes.NewReader(buf.Bytes()))
s := pkg.NewDirScanner(bytes.NewReader(buf.Bytes()), true)
var got []pkg.FlatEntry
for s.Scan() {
got = append(got, *s.Entry())