Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.7.3 - Unreleased

### Fixes

- Confined Slack Desktop snapshot reads to the discovered profile root and rejected symlink or special-file entries.

### Maintenance

- Updated crawlkit through 0.12.2 for shared runtime hardening, SQLite 1.52, and absolute Windows database paths.
Expand Down
47 changes: 35 additions & 12 deletions internal/slackdesktop/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,16 @@ func SnapshotPath(path string) (snapshot Snapshot, err error) {
if err := os.MkdirAll(target, 0o750); err != nil {
return Snapshot{}, err
}
sourceRoot, err := os.OpenRoot(path)
if err != nil {
return Snapshot{}, err
}
defer func() { _ = sourceRoot.Close() }()
targetRoot, err := os.OpenRoot(target)
if err != nil {
return Snapshot{}, err
}
defer func() { _ = targetRoot.Close() }()

copyTargets := []string{
rootStateFile,
Expand All @@ -282,15 +292,14 @@ func SnapshotPath(path string) (snapshot Snapshot, err error) {
indexedDBBlobDir,
}
for _, relative := range copyTargets {
src := filepath.Join(path, filepath.FromSlash(relative))
if _, err := os.Stat(src); err != nil {
relative = filepath.FromSlash(relative)
if _, err := sourceRoot.Lstat(relative); err != nil {
if os.IsNotExist(err) {
continue
}
return Snapshot{}, err
}
dst := filepath.Join(target, filepath.FromSlash(relative))
if err := copyPath(src, dst); err != nil {
if err := copyPath(sourceRoot, targetRoot, relative); err != nil {
return Snapshot{}, err
}
}
Expand Down Expand Up @@ -1283,34 +1292,48 @@ func intString(value int) string {
return string(data)
}

func copyPath(src string, dst string) error {
info, err := os.Stat(src)
func copyPath(srcRoot *os.Root, dstRoot *os.Root, relative string) error {
info, err := srcRoot.Lstat(relative)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 {
return errors.New("desktop snapshot source contains a symbolic link")
}
if info.IsDir() {
if err := os.MkdirAll(dst, info.Mode()); err != nil {
if err := dstRoot.MkdirAll(relative, info.Mode().Perm()); err != nil {
return err
}
entries, err := os.ReadDir(src)
dir, err := srcRoot.Open(relative)
if err != nil {
return err
}
entries, readErr := dir.ReadDir(-1)
closeErr := dir.Close()
if readErr != nil {
return readErr
}
if closeErr != nil {
return closeErr
}
for _, entry := range entries {
if err := copyPath(filepath.Join(src, entry.Name()), filepath.Join(dst, entry.Name())); err != nil {
if err := copyPath(srcRoot, dstRoot, filepath.Join(relative, entry.Name())); err != nil {
return err
}
}
return nil
}
if err := os.MkdirAll(filepath.Dir(dst), 0o750); err != nil {
if !info.Mode().IsRegular() {
return errors.New("desktop snapshot source contains a special file")
}
if err := dstRoot.MkdirAll(filepath.Dir(relative), 0o750); err != nil {
return err
}
data, err := os.ReadFile(src) //nolint:gosec // Snapshot copy reads from discovered Slack desktop paths.
data, err := srcRoot.ReadFile(relative)
if err != nil {
return err
}
return os.WriteFile(dst, data, info.Mode())
return dstRoot.WriteFile(relative, data, info.Mode().Perm())
}

func cleanKey(key []byte) string {
Expand Down
8 changes: 5 additions & 3 deletions internal/slackdesktop/desktop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,13 @@ func TestDiscoverEmptyPathIsUnavailable(t *testing.T) {
require.Empty(t, source.Path)
}

func TestSnapshotPathRemovesPartialSnapshotOnError(t *testing.T) {
func TestSnapshotPathRejectsEscapingSymlinkAndRemovesPartialSnapshot(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "storage"), 0o750))
loopPath := filepath.Join(root, "storage", "root-state.json")
if err := os.Symlink("root-state.json", loopPath); err != nil {
external := filepath.Join(t.TempDir(), "outside.json")
require.NoError(t, os.WriteFile(external, []byte(`{"outside":true}`), 0o600))
symlinkPath := filepath.Join(root, "storage", "root-state.json")
if err := os.Symlink(external, symlinkPath); err != nil {
t.Skipf("symlink unavailable: %v", err)
}

Expand Down