Skip to content

fix: block path traversal via spec name/version in persistence - #114

Merged
sorenwacker merged 1 commit into
mainfrom
fix-spec-persistence-traversal
Jul 19, 2026
Merged

fix: block path traversal via spec name/version in persistence#114
sorenwacker merged 1 commit into
mainfrom
fix-spec-persistence-traversal

Conversation

@sorenwacker

Copy link
Copy Markdown
Owner

Security fix — path traversal in spec persistence (audit finding)

metaseed.specs.persistence built filesystem paths from a spec's version (and, on delete, its name) without sanitization, so a crafted value escaped the custom-specs directory.

  • save_spec sanitized the profile name but interpolated spec.version raw: get_custom_specs_dir() / safe_name / spec.versionmkdir(parents=True) + write profile.yaml. version="../../.config/autostart" writes a profile.yaml into an attacker-chosen directory (autostart → persistence). Reachable via the MCP spec_save tool, where version is model-controlled (spec_set_metadata / spec_clone).
  • delete_user_spec interpolated name and version raw before shutil.rmtree(...) — a destructive traversal.

Fix

New _specs_subpath(*parts) resolves the path under get_custom_specs_dir() and raises ValueError unless it stays within the (resolved) base (Path.is_relative_to), also rejecting empty components. Both save_spec (version) and delete_user_spec (name + version, before any rmtree) route through it. Blocks .., absolute paths, and symlink escape in one place. Same containment approach as the dataset-name fix in #100, adapted since versions legitimately contain dots.

Tests (tests/test_specs/test_persistence_security.py)

PoC-style, contained in tmp_path (monkeypatched get_custom_specs_dir):

  • save_spec with a relative-traversal, absolute, and empty version → raises, and no file appears outside the base.
  • delete_user_spec with a traversal version and a traversal name → raises and a planted sentinel dir outside the base is untouched (rmtree never reached).
  • Controls: a normal version="1.0" saves and deletes correctly.

tests/test_specs (320) pass; ruff + strict mypy clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FTZQT3bQzqTBXQXvDsk4w9

…+ delete)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTZQT3bQzqTBXQXvDsk4w9
@sorenwacker
sorenwacker merged commit 5d7f202 into main Jul 19, 2026
2 checks passed
@sorenwacker
sorenwacker deleted the fix-spec-persistence-traversal branch July 19, 2026 13:46
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