c20ff56e4a
manifest.AppName / HDDSubdirs / VolumeNames are attacker-controlled JSON inside an imported .fab and reach filepath.Join+MkdirAll/extractTar with a trusted base (restore.go:339/606/678). UnmarshalManifest did zero validation, so '../..' in any of them escaped the stacks / HDD destination dir. - New appexport.ValidateSegment + validateManifestPaths; UnmarshalManifest now fails the parse on a traversal segment (the chokepoint). - Defence-in-depth ValidateSegment guards at the HDD-subdir and volume-name join loops in restore.go. - ConfigFiles deliberately NOT validated (holds dotfiles like .felhom.yml; never used in a restore join). - Permanent regression test (was the deep-sweep failing audit test) now asserts rejection of traversal + acceptance of legit names. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49 lines
1.6 KiB
Go
49 lines
1.6 KiB
Go
package appexport
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
// ManifestVersion is the current bundle format version.
|
|
const ManifestVersion = 1
|
|
|
|
// Manifest is the JSON metadata stored inside a .fab file.
|
|
type Manifest struct {
|
|
Version int `json:"version"`
|
|
AppName string `json:"app_name"`
|
|
DisplayName string `json:"display_name"`
|
|
ExportedAt time.Time `json:"exported_at"`
|
|
ControllerVer string `json:"controller_version"`
|
|
NeedsHDD bool `json:"needs_hdd"`
|
|
Encrypted bool `json:"encrypted"`
|
|
HasDatabase bool `json:"has_database"`
|
|
HasHDDData bool `json:"has_hdd_data"`
|
|
HasVolumeData bool `json:"has_volume_data"`
|
|
DBType string `json:"db_type,omitempty"`
|
|
TotalSizeBytes int64 `json:"total_size_bytes"`
|
|
ConfigFiles []string `json:"config_files"`
|
|
VolumeNames []string `json:"volume_names,omitempty"`
|
|
HDDSubdirs []string `json:"hdd_subdirs,omitempty"`
|
|
}
|
|
|
|
// Marshal returns the manifest as indented JSON.
|
|
func (m *Manifest) Marshal() ([]byte, error) {
|
|
return json.MarshalIndent(m, "", " ")
|
|
}
|
|
|
|
// UnmarshalManifest parses a manifest from JSON bytes.
|
|
func UnmarshalManifest(data []byte) (*Manifest, error) {
|
|
var m Manifest
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
return nil, err
|
|
}
|
|
// [CTRL-001] Reject path-traversal in any segment used to build a filesystem
|
|
// path on import (app_name, hdd_subdirs, volume_names). A hostile .fab must
|
|
// fail to parse rather than escape the stacks / HDD destination dir.
|
|
if err := validateManifestPaths(&m); err != nil {
|
|
return nil, err
|
|
}
|
|
return &m, nil
|
|
}
|