Compare commits

...
Sign in to create a new pull request.

1 commit
dev ... main

Author SHA1 Message Date
Virgil
61fb52e8f2 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>
2026-03-29 15:33:52 +00:00

View file

@ -237,15 +237,15 @@ func createTarball(files map[string][]byte) ([]byte, error) {
Size: int64(len(content)), Size: int64(len(content)),
} }
if err := tw.WriteHeader(hdr); err != nil { 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 { 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 { if err := tw.Close(); err != nil {
return nil, err return nil, coreerr.E("createTarball", "failed to close tar writer", err)
} }
return buf.Bytes(), nil return buf.Bytes(), nil
@ -261,11 +261,11 @@ func extractTarball(tarData []byte, destDir string) (string, error) {
absDestDir = filepath.Clean(absDestDir) absDestDir = filepath.Clean(absDestDir)
if err := coreio.Local.EnsureDir(absDestDir); err != nil { 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)) tr := tar.NewReader(bytes.NewReader(tarData))
var firstExecutable string var firstExecutablePath string
for { for {
hdr, err := tr.Next() hdr, err := tr.Next()
@ -273,7 +273,7 @@ func extractTarball(tarData []byte, destDir string) (string, error) {
break break
} }
if err != nil { 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) // 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 { switch hdr.Typeflag {
case tar.TypeDir: case tar.TypeDir:
if err := coreio.Local.EnsureDir(fullPath); err != nil { if err := coreio.Local.EnsureDir(fullPath); err != nil {
return "", err return "", coreerr.E("extractTarball", "failed to create directory "+cleanName, err)
} }
case tar.TypeReg: case tar.TypeReg:
// Ensure parent directory exists // Ensure parent directory exists
if err := coreio.Local.EnsureDir(filepath.Dir(fullPath)); err != nil { 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 // 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 const maxFileSize int64 = 100 * 1024 * 1024
limitedReader := io.LimitReader(tr, maxFileSize+1) limitedReader := io.LimitReader(tr, maxFileSize+1)
written, err := io.Copy(f, limitedReader) written, err := io.Copy(f, limitedReader)
f.Close()
if err != nil { if err != nil {
_ = f.Close()
return "", coreerr.E("extractTarball", "failed to write file "+hdr.Name, err) 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 { 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) return "", coreerr.E("extractTarball", "file "+hdr.Name+" exceeds maximum size", nil)
} }
// Track first executable // Track first executable
if hdr.Mode&0111 != 0 && firstExecutable == "" { if hdr.Mode&0111 != 0 && firstExecutablePath == "" {
firstExecutable = fullPath firstExecutablePath = fullPath
} }
// Explicitly ignore symlinks and hard links to prevent symlink attacks // Explicitly ignore symlinks and hard links to prevent symlink attacks
case tar.TypeSymlink, tar.TypeLink: 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). // StreamBundle writes a bundle to a writer (for large transfers).
func StreamBundle(bundle *Bundle, w io.Writer) error { func StreamBundle(bundle *Bundle, w io.Writer) error {
encoder := json.NewEncoder(w) 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. // ReadBundle reads a bundle from a reader.
@ -355,7 +365,7 @@ func ReadBundle(r io.Reader) (*Bundle, error) {
var bundle Bundle var bundle Bundle
decoder := json.NewDecoder(r) decoder := json.NewDecoder(r)
if err := decoder.Decode(&bundle); err != nil { if err := decoder.Decode(&bundle); err != nil {
return nil, err return nil, coreerr.E("ReadBundle", "failed to decode bundle", err)
} }
return &bundle, nil return &bundle, nil
} }