Skip to content

fix(F7): canonicalize paths in permission check_path (follow symlinks) - #82

Merged
yogthos merged 1 commit into
mainfrom
fix/f7-symlink-canonicalize
May 21, 2026
Merged

fix(F7): canonicalize paths in permission check_path (follow symlinks)#82
yogthos merged 1 commit into
mainfrom
fix/f7-symlink-canonicalize

Conversation

@yogthos

@yogthos yogthos commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Track F-HIGH #7. Previously a symlink could bypass deny rules: ln -s /etc/passwd benign-name matched against pattern rules as benign-name. Now resolve_absolute canonicalizes (resolves symlinks + .. ) with three-level fallback for nonexistent paths. 2 new tests, 662 pass.

Track F-HIGH #7 from ROADMAP.md.

## Problem

`resolve_absolute` (`permission/checker.rs:370-376`) joined
relative paths against working_dir but never followed symlinks.
A user could create:

  ln -s /etc/passwd ./benign-name.txt

…and any deny rule on `/etc/**` would be bypassed because the
pattern matcher only saw `./benign-name.txt`. Same hole for
external-directory rules in `Accept` mode.

## Fix

`resolve_absolute` now calls `std::fs::canonicalize` on the
joined path. canonicalize() resolves symlinks AND normalizes
`.` / `..` lexically — the path the rule matches is now the
real target, not the link name.

Three-level fallback for nonexistent paths (writes to new files
need to resolve too):

1. Try `canonicalize(full_path)` — works for existing paths +
   symlinks.
2. If that fails (NotFound on write-to-new), try
   `canonicalize(parent) + basename` — catches
   `/safe/parent/../../etc/passwd` style attacks where the
   parent exists.
3. If even the parent can't canonicalize (test fixture dirs
   that don't exist on disk), fall back to the lexical join.
   Matches pre-F7 behavior so existing permission tests on
   not-yet-existing paths still match.

## Tests

Two new tests in `permission::checker::tests`:

- `resolve_absolute_follows_symlinks`: creates a tempdir with
  `real-secret.txt` and a `benign-name.txt` symlink to it;
  asserts resolve returns the real target's canonical path,
  not the link name. Handles macOS `/tmp -> /private/tmp` by
  canonicalizing the expected path too.
- `resolve_absolute_handles_nonexistent_via_parent_canonicalize`:
  parent dir exists but the leaf doesn't; asserts the result is
  `canonical(parent) / leaf` — proves the second-level fallback
  works without breaking parent resolution.

662 pass (was 660). All build profiles clean.
@yogthos
yogthos merged commit 4de39be into main May 21, 2026
1 check passed
@yogthos
yogthos deleted the fix/f7-symlink-canonicalize branch May 21, 2026 04:26
allen-munsch pushed a commit to allen-munsch/dirge that referenced this pull request Jun 3, 2026
…ks (dirge-code#82)

Track F-HIGH dirge-code#7 from ROADMAP.md.

## Problem

`resolve_absolute` (`permission/checker.rs:370-376`) joined
relative paths against working_dir but never followed symlinks.
A user could create:

  ln -s /etc/passwd ./benign-name.txt

…and any deny rule on `/etc/**` would be bypassed because the
pattern matcher only saw `./benign-name.txt`. Same hole for
external-directory rules in `Accept` mode.

## Fix

`resolve_absolute` now calls `std::fs::canonicalize` on the
joined path. canonicalize() resolves symlinks AND normalizes
`.` / `..` lexically — the path the rule matches is now the
real target, not the link name.

Three-level fallback for nonexistent paths (writes to new files
need to resolve too):

1. Try `canonicalize(full_path)` — works for existing paths +
   symlinks.
2. If that fails (NotFound on write-to-new), try
   `canonicalize(parent) + basename` — catches
   `/safe/parent/../../etc/passwd` style attacks where the
   parent exists.
3. If even the parent can't canonicalize (test fixture dirs
   that don't exist on disk), fall back to the lexical join.
   Matches pre-F7 behavior so existing permission tests on
   not-yet-existing paths still match.

## Tests

Two new tests in `permission::checker::tests`:

- `resolve_absolute_follows_symlinks`: creates a tempdir with
  `real-secret.txt` and a `benign-name.txt` symlink to it;
  asserts resolve returns the real target's canonical path,
  not the link name. Handles macOS `/tmp -> /private/tmp` by
  canonicalizing the expected path too.
- `resolve_absolute_handles_nonexistent_via_parent_canonicalize`:
  parent dir exists but the leaf doesn't; asserts the result is
  `canonical(parent) / leaf` — proves the second-level fallback
  works without breaking parent resolution.

662 pass (was 660). All build profiles clean.

Co-authored-by: Yogthos <yogthos@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant