fix(ingestion): every brain/raw/ reader goes through an os.Root; symlinks are inert
raw/ is writable via Syncthing (a sendreceive folder shared with flamingo, symlinks synced) and by the brain CronJobs, and several readers followed symlinks out of it: - /pending and brain_pending showed a symlink target's contents; brain_promote would copy it into wiki/ (then brain-sync to Gitea); brain_reject would copy it into raw/rejected/. - The watcher, which needs no API call at all, ingested symlinked files (WalkDir yields them; extract.Text followed them). Not in the issue; found while fixing it. - Ingest checked a path then reopened it by name (TOCTOU). OpenRawRoot opens brain/raw as an os.Root and refuses a raw/ that is itself a symlink. ListPending, PromoteNote and RejectNote read through it and refuse anything not a regular file. extract.TextIn reads through a root (temp copy for pdf/docmark); /ingest-path, brain_ingest and the watcher use it, and the watcher archives via the root too, so a file swapped for a symlink after a check still cannot lead out. Refs #153 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package extract
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// hyperguild#153 item 3: ingest checked a path, then extract.Text reopened it
|
||||
// by name, so a file swapped for a symlink in between was followed out of
|
||||
// raw/. TextIn reads through an os.Root, which refuses to leave the root.
|
||||
func TestTextInReadsInsideTheRootAndRefusesEscapes(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(root, "ok.md"), []byte("inside"), 0o644))
|
||||
outside := filepath.Join(t.TempDir(), "secret.md")
|
||||
require.NoError(t, os.WriteFile(outside, []byte("SENTINEL-153-TEXTIN"), 0o644))
|
||||
require.NoError(t, os.Symlink(outside, filepath.Join(root, "leak.md")))
|
||||
|
||||
r, err := os.OpenRoot(root)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = r.Close() }()
|
||||
|
||||
got, err := TextIn(r, "ok.md")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "inside", got)
|
||||
|
||||
got, err = TextIn(r, "leak.md")
|
||||
assert.Error(t, err)
|
||||
assert.NotContains(t, got, "SENTINEL-153-TEXTIN")
|
||||
|
||||
_, err = TextIn(r, "../"+filepath.Base(outside))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user