From a1e2bc2100c68788489beca2dfcc3cf72c76f14f Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars Date: Tue, 16 Jun 2026 11:19:36 -0400 Subject: [PATCH] fix(security): slugify entry_type before using it in the fabric filename `write_entry` interpolates `entry_type` directly into the output filename (`f"{agent}-{entry_type}-{slug}-{suffix}.md"`). `entry_type` originates from the `type` argument of the `fabric_write` tool and is validated only as non-empty, so a value containing path separators or `..` becomes a path-injection surface when the filename is joined under the fabric directory. The `summary`-derived `slug` is already sanitized via `re.sub(r"[^a-z0-9]+", ...)`; apply the same slugify to `entry_type` for the filename component. The real `entry_type` value is unchanged everywhere else (YAML `type:` field, the `type == "review"` logic), so behavior is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) --- icarus/state.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/icarus/state.py b/icarus/state.py index 86390ec..1873219 100644 --- a/icarus/state.py +++ b/icarus/state.py @@ -321,10 +321,14 @@ def write_entry(entry_type, content, summary, tier="hot", tags="", platform="cli suffix = secrets.token_hex(2) # derive a short slug from the summary for human-readable filenames slug = re.sub(r"[^a-z0-9]+", "-", summary.lower().strip())[:40].strip("-") + # entry_type is caller/tool-controlled and is used as a path component below; + # slugify it the same way as the summary so it can't introduce path separators + # or traversal sequences into the filename. + type_slug = re.sub(r"[^a-z0-9]+", "-", entry_type.lower().strip())[:32].strip("-") or "entry" if slug: - filename = f"{agent}-{entry_type}-{slug}-{suffix}.md" + filename = f"{agent}-{type_slug}-{slug}-{suffix}.md" else: - filename = f"{agent}-{entry_type}-{ts}-{suffix}.md" + filename = f"{agent}-{type_slug}-{ts}-{suffix}.md" sid = session_id or os.environ.get( "FABRIC_SESSION_ID", f"sess-{now.strftime('%Y%m%d-%H%M%S')}-{os.getpid()}")