fix(node): harden bundle stream error handling
All checks were successful
Security Scan / security (push) Successful in 14s
Test / test (push) Successful in 2m13s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-29 15:33:52 +00:00
parent 727b5fdb8d
commit 61fb52e8f2

View file

@ -237,15 +237,15 @@ func createTarball(files map[string][]byte) ([]byte, error) {
Size: int64(len(content)),
}
if err := tw.WriteHeader(hdr); err != nil {
return nil, err
return nil, coreerr.E("createTarball", "failed to write tar header", err)
}
if _, err := tw.Write(content); err != nil {
return nil, err
return nil, coreerr.E("createTarball", "failed to write tar content", err)
}
}
if err := tw.Close(); err != nil {
return nil, err
return nil, coreerr.E("createTarball", "failed to close tar writer", err)
}
return buf.Bytes(), nil
@ -261,11 +261,11 @@ func extractTarball(tarData []byte, destDir string) (string, error) {
absDestDir = filepath.Clean(absDestDir)
if err := coreio.Local.EnsureDir(absDestDir); err != nil {
return "", err
return "", coreerr.E("extractTarball", "failed to ensure destination directory", err)
}
tr := tar.NewReader(bytes.NewReader(tarData))
var firstExecutable string
var firstExecutablePath string
for {
hdr, err := tr.Next()
@ -273,7 +273,7 @@ func extractTarball(tarData []byte, destDir string) (string, error) {
break
}
if err != nil {
return "", err
return "", coreerr.E("extractTarball", "failed to read tar entry", err)
}
// Security: Sanitize the tar entry name to prevent path traversal (Zip Slip)
@ -301,12 +301,12 @@ func extractTarball(tarData []byte, destDir string) (string, error) {
switch hdr.Typeflag {
case tar.TypeDir:
if err := coreio.Local.EnsureDir(fullPath); err != nil {
return "", err
return "", coreerr.E("extractTarball", "failed to create directory "+cleanName, err)
}
case tar.TypeReg:
// Ensure parent directory exists
if err := coreio.Local.EnsureDir(filepath.Dir(fullPath)); err != nil {
return "", err
return "", coreerr.E("extractTarball", "failed to create parent directory for "+cleanName, err)
}
// os.OpenFile is used deliberately here instead of coreio.Local.Create/Write
@ -321,18 +321,24 @@ func extractTarball(tarData []byte, destDir string) (string, error) {
const maxFileSize int64 = 100 * 1024 * 1024
limitedReader := io.LimitReader(tr, maxFileSize+1)
written, err := io.Copy(f, limitedReader)
f.Close()
if err != nil {
_ = f.Close()
return "", coreerr.E("extractTarball", "failed to write file "+hdr.Name, err)
}
if err := f.Close(); err != nil {
return "", coreerr.E("extractTarball", "failed to close extracted file "+hdr.Name, err)
}
if written > maxFileSize {
coreio.Local.Delete(fullPath)
if err := coreio.Local.Delete(fullPath); err != nil {
return "", coreerr.E("extractTarball", "failed to clean up oversized file "+hdr.Name, err)
}
return "", coreerr.E("extractTarball", "file "+hdr.Name+" exceeds maximum size", nil)
}
// Track first executable
if hdr.Mode&0111 != 0 && firstExecutable == "" {
firstExecutable = fullPath
if hdr.Mode&0111 != 0 && firstExecutablePath == "" {
firstExecutablePath = fullPath
}
// Explicitly ignore symlinks and hard links to prevent symlink attacks
case tar.TypeSymlink, tar.TypeLink:
@ -341,13 +347,17 @@ func extractTarball(tarData []byte, destDir string) (string, error) {
}
}
return firstExecutable, nil
return firstExecutablePath, nil
}
// StreamBundle writes a bundle to a writer (for large transfers).
func StreamBundle(bundle *Bundle, w io.Writer) error {
encoder := json.NewEncoder(w)
return encoder.Encode(bundle)
if err := encoder.Encode(bundle); err != nil {
return coreerr.E("StreamBundle", "failed to encode bundle", err)
}
return nil
}
// ReadBundle reads a bundle from a reader.
@ -355,7 +365,7 @@ func ReadBundle(r io.Reader) (*Bundle, error) {
var bundle Bundle
decoder := json.NewDecoder(r)
if err := decoder.Decode(&bundle); err != nil {
return nil, err
return nil, coreerr.E("ReadBundle", "failed to decode bundle", err)
}
return &bundle, nil
}