fix(agents): block directory traversal in command write paths (#2229)#2296
Merged
mnriem merged 1 commit intogithub:mainfrom Apr 21, 2026
Merged
fix(agents): block directory traversal in command write paths (#2229)#2296mnriem merged 1 commit intogithub:mainfrom
mnriem merged 1 commit intogithub:mainfrom
Conversation
…#2229) Extend the alias containment guard from b67b285 to the two remaining write paths that derive filenames from free-form command/alias names: - Primary command write in CommandRegistrar.register_commands() - CommandRegistrar.write_copilot_prompt() Consolidate the check into a shared _ensure_inside() helper. Per maintainer guidance on github#2229, use a lexical (os.path.normpath + Path.is_relative_to) containment check rather than resolve() so `..` / absolute-path traversal is rejected while intentionally symlinked sub-directories under an agent's commands directory (e.g. .claude/skills/shared -> /team/shared-skills) keep working for existing extension setups. Add 22 parametrised regression cases covering traversal payloads on primary commands, aliases, and the Copilot companion prompt, plus a positive case that confirms symlinked sub-directories remain supported.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens CommandRegistrar against directory traversal by enforcing lexical “stay within base dir” checks for all command and Copilot prompt write paths, closing #2229 while preserving workflows that rely on symlinked subdirectories.
Changes:
- Add a shared
_ensure_inside()helper that performs lexical containment checks (normpath+Path.is_relative_to) and apply it to primary command writes, alias writes, and Copilot prompt writes. - Align the existing alias containment guard to the same lexical policy for consistent behavior across write sites.
- Add a focused regression test suite covering traversal payloads, safe baselines, and symlinked-subdir compatibility.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/specify_cli/agents.py |
Centralizes and applies lexical containment checks to all relevant write targets in CommandRegistrar. |
tests/test_registrar_path_traversal.py |
Adds regression tests ensuring traversal payloads are rejected and symlinked subdirs remain supported. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Collaborator
|
Thank you! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #2229.
Extends the alias containment guard from
b67b285(Goose PR #2015) to the two remaining write paths inCommandRegistrarthat derive filenames from free-form command or alias names:register_commands()— previously unguarded; a primary command name like/absolute/evilcould escapecommands_dirand attempt to create parent directories outside it.write_copilot_prompt()— companion.prompt.mdwrites under.github/prompts/previously had no containment check.Both are now protected by a shared
_ensure_inside()helper.Design decision
Per maintainer guidance on #2229, this uses a lexical containment check (
os.path.normpath+Path.is_relative_to) rather than a resolved-path check. Path traversal via..or absolute paths is blocked at all three write sites, while intentionally symlinked sub-directories under an agent's commands directory (for example,.claude/skills/shared -> /team/shared-skills) remain supported to preserve backwards compatibility for existing extension setups.This also aligns the existing alias guard from
b67b285with the same lexical policy so all three write sites behave consistently.Tests
Added 22 regression cases in
tests/test_registrar_path_traversal.py:b67b285write_copilot_promptdirect@staticmethod, new coveragePayloads:
../pwned,../../etc/passwd,subdir/../../escape,/absolute/evil.Validated locally with:
uv run pytest -q tests/test_registrar_path_traversal.pyuv run pytest -q tests/test_extensions.py -k 'copilot_companion_prompt_created or copilot_aliases_get_companion_prompts or register_commands_for_copilot'uvx ruff check src/specify_cli/agents.py tests/test_registrar_path_traversal.pyOut of scope
commands_dir) — not addressed by the lexical containment policy chosen for backwards compatibility.ExtensionManager.install_from_directory()flows are still validated upstream byEXTENSION_COMMAND_NAME_PATTERN; this PR adds defense in depth for direct callers of the publicregister_commands()API.