Files
felhom-controller/controller/internal/appexport/manifest.go
T
admin c20ff56e4a fix(CTRL-001): reject path traversal in .fab import manifest
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>
2026-06-13 19:09:47 +02:00

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
}