Skip to content

Commit 0aca0d4

Browse files
pandas-studioclaude
andcommitted
fixup(c1a): defense-in-depth validate_name in render_seed_bytes + follow-up notes
PR #628 self-review: ``render_seed_bytes`` is in ``__all__`` and constructs ``store.root / asset_type / name / ...`` paths from the ``name`` parameter. ``seed_override`` (the usual caller) already validates, but a direct call with ``name = "../../etc/passwd"`` would otherwise traverse out of the wiki root. PR-D C1a is the first PR where the agents/commands path-construction code is live (PR-D-prep #627 added it under the ``_PR_C_ACTIVE_TYPES`` gate), so this is the right ship to defend at the function boundary rather than relying on caller discipline. - ``render_seed_bytes`` calls ``validate_name`` on entry. Idempotent with ``seed_override``'s validate (same kind string, same name). - New test ``test_render_seed_bytes_rejects_traversal_name`` covers all 3 asset types with traversal-shaped names. - Inline comment near the ``_dropped`` discard sites flags C1b as the follow-up that will surface dropped vendor fields via stderr WARNING. Co-Authored-By: Claude <[email protected]>
1 parent 4e7eb7e commit 0aca0d4

2 files changed

Lines changed: 33 additions & 0 deletions

File tree

packages/memtomem/src/memtomem/wiki/override.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,14 @@ def render_seed_bytes(
4242
Skills → canonical ``SKILL.md``.
4343
Agents / commands → ``parse_canonical_*`` + vendor renderer.
4444
``("commands", "codex")`` → :class:`NotImplementedError`.
45+
46+
``name`` is validated here even though :func:`seed_override` (the usual
47+
caller) already validates — the function is in ``__all__`` so direct
48+
callers should not have to remember to pre-validate. Defense in depth
49+
for the ``store.root / asset_type / name / ...`` path joins below.
4550
"""
51+
validate_name(name, kind=f"{asset_type.removesuffix('s')} name")
52+
4653
if asset_type == "skills":
4754
src = store.root / "skills" / name / "SKILL.md"
4855
if not src.is_file():
@@ -55,6 +62,12 @@ def render_seed_bytes(
5562
f"wiki has no {asset_type}/{name}/{asset_type[:-1]}.md to seed from at {canonical}"
5663
)
5764

65+
# ``gen.render()`` returns ``(text, dropped_field_names)`` — fields the
66+
# vendor format can't represent (e.g., gemini drops ``tools`` /
67+
# ``skills`` / ``model``). C1a silently discards ``_dropped``; C1b CLI
68+
# will surface them via stderr WARNING so users editing the override
69+
# know which fields the runtime won't see.
70+
#
5871
# Function-body imports dodge a wiki ↔ context import cycle:
5972
# ``context.install`` already imports ``wiki.store``; widening to
6073
# module-top imports here would close the loop.

packages/memtomem/tests/test_wiki_override.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import pytest
1515

16+
from memtomem.context._names import InvalidNameError
1617
from memtomem.wiki.override import render_seed_bytes
1718
from memtomem.wiki.store import WikiStore
1819

@@ -98,3 +99,22 @@ def test_render_seed_bytes_codex_commands_raises_not_implemented(
9899

99100
with pytest.raises(NotImplementedError, match="commands not yet supported"):
100101
render_seed_bytes(store, "commands", "baz", "codex")
102+
103+
104+
def test_render_seed_bytes_rejects_traversal_name(wiki_root: Path) -> None:
105+
"""Defense-in-depth: ``render_seed_bytes`` validates ``name`` itself.
106+
107+
``seed_override`` (the usual caller) already validates, but
108+
``render_seed_bytes`` is in ``__all__`` so direct callers should not
109+
have to remember to pre-validate. A traversal-shaped name in the
110+
``store.root / asset_type / name / ...`` path would otherwise escape
111+
the wiki root.
112+
"""
113+
store = _initialized_wiki()
114+
115+
with pytest.raises(InvalidNameError):
116+
render_seed_bytes(store, "agents", "../etc/passwd", "claude")
117+
with pytest.raises(InvalidNameError):
118+
render_seed_bytes(store, "commands", "../../escape", "claude")
119+
with pytest.raises(InvalidNameError):
120+
render_seed_bytes(store, "skills", "../../escape", "claude")

0 commit comments

Comments
 (0)