Skip to content

feat: file-access confinement for clet md (security boundary)#96

Merged
tig merged 20 commits intodevelopfrom
copilot/fix-clet-md-file-access-issue
May 6, 2026
Merged

feat: file-access confinement for clet md (security boundary)#96
tig merged 20 commits intodevelopfrom
copilot/fix-clet-md-file-access-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 6, 2026

clet md FILE is an arbitrary file-read primitive. In agent contexts, positional args may be attacker-influenced via indirect prompt injection (e.g. clet md ~/.aws/credentials), exfiltrating content through rendered ANSI or --cat stdout.

Changes

FileAccessPolicy (new, src/Clet/Hosting/FileAccessPolicy.cs)

Default-deny policy enforced on both the --cat path (AliasDispatcher.ResolveViewerContent) and TUI path (MarkdownClet.ExpandFiles):

  • Extension allowlist: .md, .markdown, .txt
  • Working directory confinement (full-path normalized comparison)
  • Per-file 16 MiB / aggregate 32 MiB / glob 128-file caps
  • Binary rejection (NUL in first 8 KiB)

Escape hatches (CommandLineRoot parsing)

  • --allow-file <path> (repeatable) — bypasses extension + cwd checks
  • --allow-binary — disables NUL detection
# Refused by default:
clet md /etc/passwd

# Explicit opt-in:
clet md --allow-file /etc/passwd /etc/passwd

Surface 2: pick-file/pick-directory --root

Documented honestly as starting directory, not a sandbox. Help text updated to: "Starting directory (not a sandbox — user can navigate freely)."

Documentation

  • docs/threat-model.md "File access scope" rewritten with full confinement model
  • specs/decisions.md D-032 appended (renumbered from D-030 → D-031 → D-032 to avoid conflicts with terminal escape sanitization and link policy entries)

Tests

  • FileAccessPolicyTests — extension/cwd/binary/size/glob unit coverage
  • FileAccessPolicySmokeTests — process-level refusal and bypass verification

Copilot AI and others added 2 commits May 6, 2026 20:09
- Add FileAccessPolicy with extension allowlist, cwd confinement, size caps, binary rejection
- Add --allow-file and --allow-binary CLI flags
- Integrate policy into AliasDispatcher.ResolveViewerContent and MarkdownClet.ExpandFiles
- Document --root as starting-only (not sandbox) for pick-file/pick-directory
- Add D-030 decisions entry
- Update docs/threat-model.md "File access scope" section
- Add unit tests (FileAccessPolicyTests) and smoke tests (FileAccessPolicySmokeTests)

Agent-Logs-Url: https://github.com/gui-cs/clet/sessions/02d84ff1-fcf6-4d66-989b-67fecf6fd033

Co-authored-by: tig <585482+tig@users.noreply.github.com>
…move duplicate check

Agent-Logs-Url: https://github.com/gui-cs/clet/sessions/02d84ff1-fcf6-4d66-989b-67fecf6fd033

Co-authored-by: tig <585482+tig@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix security issue with clet md file access feat: file-access confinement for clet md (security boundary) May 6, 2026
Copilot finished work on behalf of tig May 6, 2026 20:13
Copilot AI requested a review from tig May 6, 2026 20:13
Comment thread tests/Clet.SmokeTests/FileAccessPolicySmokeTests.cs Fixed
Comment thread tests/Clet.SmokeTests/FileAccessPolicySmokeTests.cs Fixed
Comment thread tests/Clet.SmokeTests/FileAccessPolicySmokeTests.cs Fixed
Comment thread tests/Clet.SmokeTests/FileAccessPolicySmokeTests.cs Fixed
Comment thread tests/Clet.SmokeTests/FileAccessPolicySmokeTests.cs Fixed
Comment thread tests/Clet.SmokeTests/FileAccessPolicySmokeTests.cs Fixed
Comment thread src/Clet/Hosting/AliasDispatcher.cs Fixed
Comment thread src/Clet/Hosting/FileAccessPolicy.cs Fixed
Comment thread tests/Clet.SmokeTests/FileAccessPolicySmokeTests.cs Fixed
Comment thread tests/Clet.SmokeTests/FileAccessPolicySmokeTests.cs Fixed
@tig tig marked this pull request as ready for review May 6, 2026 20:18
tig and others added 10 commits May 6, 2026 14:18
…ariable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
…ariable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
…ariable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
…ariable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
…ariable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
…ariable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
…ne' may silently drop its earlier arguments'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
…ne' may silently drop its earlier arguments'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Comment thread tests/Clet.SmokeTests/FileAccessPolicySmokeTests.cs Dismissed
// Create a .md file and run from its directory
string tmpDir = Path.Join (Path.GetTempPath (), $"clet-test-{Guid.NewGuid ()}");
Directory.CreateDirectory (tmpDir);
string mdFile = Path.Combine (tmpDir, "test.md");
Comment thread tests/Clet.SmokeTests/FileAccessPolicySmokeTests.cs Dismissed
Comment thread tests/Clet.SmokeTests/FileAccessPolicySmokeTests.cs Dismissed
Comment thread tests/Clet.UnitTests/CommandLineRootTests.cs Dismissed
public void AllowedExtension_InWorkingDirectory_Passes ()
{
string cwd = Path.GetTempPath ();
string file = Path.Combine (cwd, "readme.md");
Comment thread tests/Clet.UnitTests/FileAccessPolicyTests.cs Dismissed
Comment thread tests/Clet.UnitTests/FileAccessPolicyTests.cs Dismissed
Comment thread tests/Clet.UnitTests/FileAccessPolicyTests.cs Dismissed
[Fact]
public void AllowFile_BypassesExtensionAndCwdChecks ()
{
string cwd = Path.Combine (Path.GetTempPath (), "clet-test-cwd2");
@tig
Copy link
Copy Markdown
Member

tig commented May 6, 2026

@copilot tests are failing

…CS0103 build error

Agent-Logs-Url: https://github.com/gui-cs/clet/sessions/7731e404-c52b-4351-ba8b-c7560fb94836

Co-authored-by: tig <585482+tig@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 6, 2026

@copilot tests are failing

Fixed in 921f4c2. The CI error was CS0103: The name 'SecurityException' does not exist in the current context — an earlier auto-fix commit introduced SecurityException without qualifying it. Changed to System.Security.SecurityException to resolve.

Copilot finished work on behalf of tig May 6, 2026 20:30
Copilot AI requested a review from tig May 6, 2026 20:30
@tig
Copy link
Copy Markdown
Member

tig commented May 6, 2026

@copilot resolve the merge conflicts in this pull request

…d-file-access-issue

# Conflicts:
#	specs/decisions.md

Co-authored-by: tig <585482+tig@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 6, 2026

@copilot resolve the merge conflicts in this pull request

Resolved in 59f7585. The conflict was in specs/decisions.md where both branches added a D-030 entry. Kept develop's D-030 (terminal escape sanitization) and renumbered the file-access confinement entry to D-031.

Copilot finished work on behalf of tig May 6, 2026 20:35
@tig
Copy link
Copy Markdown
Member

tig commented May 6, 2026

@copilot resolve the merge conflicts in this pull request

…d-file-access-issue

# Conflicts:
#	specs/decisions.md

Co-authored-by: tig <585482+tig@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 6, 2026

@copilot resolve the merge conflicts in this pull request

Resolved in 600741b. The conflict was again in specs/decisions.md — develop added D-031 (SurfaceOnly link policy). Renumbered the file-access confinement entry to D-032.

Copilot finished work on behalf of tig May 6, 2026 21:22
Clicking a relative .md link in the viewer navigates to that file
in-place, subject to the same FileAccessPolicy confinement as file
loading (CWD sandbox, extension allowlist, size/binary checks).
Links outside the sandbox or to non-markdown files remain
SurfaceOnly (shown in status bar). HTTP/HTTPS/mailto links are
always SurfaceOnly.

Also fix integration test that used .tmp extension in temp dir,
now creates .md file in CWD to satisfy FileAccessPolicy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
--allow-file now accepts directories in addition to files. When a
directory is given, any file under that tree bypasses the extension
and CWD confinement checks (size/binary checks still apply).

This makes the common case natural:
  clet md ../Terminal.Gui/README.md --allow-file ../Terminal.Gui

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tig and others added 2 commits May 6, 2026 15:45
Replace the plain-text status shortcut with a Link CommandView.
When the user hovers a hyperlink in the markdown, the URL appears
in the status bar as a clickable link — clicking it opens in the
default browser. When viewing a file, the link shows the filename
with no URL (not clickable).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Toggle MouseHighlightStates between In (when a clickable URL is
shown) and None (when showing a filename or status text).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tig tig merged commit b0791b7 into develop May 6, 2026
5 checks passed
tig added a commit that referenced this pull request May 6, 2026
Clicking an http:// or https:// link in clet md now opens it in
the default browser via Link.OpenUrl. These are safe — worst case
the user sees a webpage. The URL is still shown in the status bar.

Local .md file navigation (within CWD sandbox) will come when
PR #96 merges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tig added a commit that referenced this pull request May 6, 2026
Merge develop which includes PR #96 (FileAccessPolicy, local .md
link navigation, --allow-file directory support).

MarkdownClet LinkClicked now:
1. Navigates local .md files within the sandbox (from #96)
2. Opens http/https links in the default browser (safe)
3. Shows the URL in the status bar as a clickable link

Resolve decisions.md conflict: linear-range is D-032, file-access
confinement is D-033.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.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.

Security: file-access boundary — clet md confinement + pick-file/pick-directory --root verification

2 participants