Check the exact persistence destination before bootstrap

Normalise the selected data directory with the storage path routine before inspection. Raw symlink/.. traversal and trailing separators must not probe an empty peer or hide a linked root while the encrypted importer writes elsewhere. Retain and validate the existing canonical agent credential.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-12 05:27:06 +01:00
parent 70c1ad1471
commit b16ef31f7b
3 changed files with 22 additions and 1 deletions
@@ -24,6 +24,11 @@
absent or empty resolved data directory; a regular deployment `.env` is the
only allowed pre-start entry. Existing non-Proxmox settings, token/session
records, encryption keys, tenant directories and unknown contents all block it.
Admission uses the same `NormalizeStorageDir` path as persistence before any
filesystem probe: raw symlink/`..` traversal must not inspect an empty peer while
the importer writes a different existing destination, and a trailing separator
must not hide a final directory link. Canonical destination credential validation
and live/dangling link spellings have explicit regression controls.
Directory inspection errors, non-directories and symlinks are not absence.
Admission reads no file contents and creates no persistence/key. The importer
rechecks before creating persistence or writing and audits a classified refusal
+8
View File
@@ -29,6 +29,7 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/securityutil"
"github.com/rcourtman/pulse-go-rewrite/internal/telemetry"
"github.com/rcourtman/pulse-go-rewrite/internal/updates"
@@ -1094,6 +1095,13 @@ var (
// A regular .env is the only exception for pre-start deployment overrides;
// keep the input archive outside the destination, not alongside retained state.
func checkAutoImportDestination(dataDir string) error {
// Inspect the exact path persistence will use. Raw symlink/.. traversal or
// trailing separators can otherwise inspect a different directory, or hide
// the final directory link from Lstat.
dataDir, err := securityutil.NormalizeStorageDir(dataDir)
if err != nil {
return errAutoImportDestinationUnknown
}
info, err := os.Lstat(dataDir)
if os.IsNotExist(err) {
return nil
+9 -1
View File
@@ -95,7 +95,7 @@ func TestStartupImportUsesPersistenceDestination(t *testing.T) {
canonical := filepath.Join(base, "destination")
target := config.NewConfigPersistence(canonical)
if err := target.SaveAPITokens([]config.APITokenRecord{{
ID: "synthetic-existing-record", Hash: "synthetic-existing-digest",
ID: "synthetic-existing-record", Hash: auth.HashAPIToken("synthetic-existing-credential"),
Scopes: []string{config.ScopeAgentReport},
}}); err != nil {
t.Fatal(err)
@@ -122,6 +122,14 @@ func TestStartupImportUsesPersistenceDestination(t *testing.T) {
if after := snapshotAutoImportDestination(t, canonical); !reflect.DeepEqual(before, after) {
t.Error("canonical destination was modified")
}
records, err := target.LoadAPITokens()
if err != nil {
t.Fatal(err)
}
loaded := &config.Config{APITokens: records}
if record, ok := loaded.ValidateAPIToken("synthetic-existing-credential"); !ok || record.ID != "synthetic-existing-record" {
t.Error("canonical destination credential no longer validates")
}
if entries, err := os.ReadDir(filepath.Join(peer, "destination")); err != nil || len(entries) != 0 {
t.Error("the peer used by raw traversal was modified")
}