T09 · Insecure Skill Coding Practices
- Location
scripts/governance.py:247- Finding
Persistent Markdown Instruction Injection Through Unsanitized Index Content
- Content
View full analysis
Vulnerability Details
File Location:
scripts/governance.py, lines 247 and 271–277, with the generated content written at lines 341–355
Vulnerability Type: Persistent Markdown and agent-instruction injection
Risk Level: HighVulnerable Code
python return {str(k): str(v) for k, v in data.items() if isinstance(v, str)}python rel = entry.relative_to(root).as_posix() name = entry.name + ("/" if entry.is_dir() else "") line = f"{prefix}{connector}[{name}]({_link_target(rel)})" note = notes.get(rel) or notes.get(rel + "/") if note: if len(note) > max_note_length: note = note[:max_note_length] + "…" line += f" — {note}" lines.append(line)python section = f"{root_section}\n```\n" + "\n".join(tree) + "\n```\n" try: text = index_path.read_text(encoding="utf-8") except UnicodeDecodeError as exc: print(f"ERROR: index.md is not valid UTF-8 text: {exc}") return 1 start = _find_heading_line(text, root_section) end = _find_heading_line(text, changelog_section) if start == -1 or end == -1 or end <= start: print(f"ERROR: index.md must contain '{root_section}' before '{changelog_section}' sections.") return 1 lines = text.splitlines(keepends=True) new_text = "".join(lines[:start]) + section + "".join(lines[end:]) index_path.write_text(new_text, encoding="utf-8")Technical Analysis
The index generator treats filesystem entry names and values loaded from
index_notes.jsonas trusted Markdown. These values are inserted directly into Markdown link labels and into a fixed triple-backtick fenced block without escaping or rejecting:- Carriage returns or line feeds
- Backtick fence delimiters
- Markdown link-control characters
- Bidirectional or other unsafe control characters
- Text that resembles agent instructions
On filesystems that permit newlines in filenames, a crafted filename can contain a line break ...[truncated 3157 chars]
- Remediation
View remediation
Remediation Suggestions
-
Reject unsafe control characters
- Reject filenames and notes containing
\r,\n, NUL, bidirectional controls, and other non-printing control characters. - Return a clear validation error instead of silently generating an unsafe index.
- Reject filenames and notes containing
-
Escape Markdown structure
- Escape Markdown link-label characters in filenames.
- Percent-encode link targets rather than placing raw paths into links.
- Ensure note text is rendered as data rather than executable Markdown.
-
Harden fenced-block generation
- Do not place untrusted text inside a fixed triple-backtick fence.
- Dynamically select a fence longer than every run of backticks in the generated content, or use a serialization format with defined escaping.
- Validate the finished section to ensure untrusted input cannot create additional headings or terminate the intended block.
-
Separate data from agent instructions
- Explicitly state that generated directory entries and notes are untrusted project data and must never be interpreted as agent directives.
- Avoid describing the generated index as an unrestricted source of authoritative instructions.
-
Validate
index_notes.jsonvalues- Enforce a schema that rejects line breaks, fence delimiters, control characters, and instruction-like multiline content.
- Apply validation before truncation so truncation cannot produce an unsafe partial sequence.
-
Add adversarial regression tests
- Test filenames containing newlines, carriage returns, triple backticks, brackets, parentheses, and bidirectional controls.
- Test notes containing fence-escape payloads and forged headings.
- Verify that generated content remains inside exactly one intended fenced block and cannot introduce executable-looking Markdown outside it.
-
