T09 · Insecure Skill Coding Practices
- Location
scripts/standup.py:106- Finding
Untrusted GitHub Content Is Forwarded into Agent-Facing Output Without Neutralization
- Content
View full analysis
Vulnerability Details
File Location:
scripts/standup.py:106-115, with output sinks atscripts/standup.py:177-185and the posting workflow atSKILL.md:29-33
Vulnerability Type: Indirect prompt injection and unsafe rendering of untrusted remote content
Risk Level: MediumVulnerable Code
python def format_pr(pr: dict) -> str: num = pr.get("number", "?") title = pr.get("title", "") url = pr.get("html_url", "") return f" • PR #{num}: {title}\n {url}" def format_commit(c: dict) -> str: sha = c.get("sha", "")[:7] msg = c.get("commit", {}).get("message", "").split("\n")[0] return f" • `{sha}` {msg}"The formatted values are subsequently written directly to standard output:
python if all_prs: print(f"### ✅ Merged PRs ({len(all_prs)})\n") for pr in all_prs: repo_label = f"[{pr['_repo']}] " if len(repos) > 1 else "" print(f"{repo_label}{format_pr(pr)}") print() if standalone_commits: print(f"### 🔧 Direct Commits ({len(standalone_commits)})\n") for c in standalone_commits: repo_label = f"[{c['_repo']}] " if len(repos) > 1 else "" print(f"{repo_label}{format_commit(c)}") print()The documented workflow directs the Agent to post this output:
markdown ## Workflow 1. Run `scripts/standup.py` with the user's repo/org and standup days 2. The script calculates the most recent past standup day and uses it as `since` 3. Output: merged PRs + direct commits (PR commits de-duped) since that date 4. Post the output as the standup messageTechnical Analysis
Pull-request titles, commit subjects, repository names, author names, and GitHub-provided URLs are remote data that may be controlled by repository contributors. The script interpolates these fields directly into Markdown-formatted output without escaping, validation, or an explicit trust-boundary marker.
...[truncated 2180 chars]
- Remediation
View remediation
Remediation Suggestions
- Treat every value returned by GitHub as untrusted data, including PR titles, commit messages, repository names, author names, and URLs.
- Return structured JSON rather than prose when output is consumed by an AI Agent. Keep remote fields in dedicated data properties and render them only through trusted presentation logic.
- Add an explicit instruction at the Agent boundary stating that report fields are untrusted records and that instructions contained in those fields must never be followed.
- Escape Markdown metacharacters in titles, commit subjects, author names, and repository labels. Validate URLs against the expected HTTPS GitHub origin before rendering them as links.
- Remove or replace control characters, bidirectional text controls, and other invisible formatting characters that could disguise content.
- Apply reasonable length limits to all remote text fields to reduce context flooding and output manipulation.
- Delimit generated records clearly, for example by placing escaped content in quoted or code-formatted fields while ensuring that delimiters inside the data cannot terminate the boundary.
- Require user confirmation before posting generated content to an external channel, especially when it contains links, mentions, or instruction-like phrases.
- Add tests using hostile PR titles and commit subjects to verify that Markdown injection, control characters, and instruction-like text remain inert data.
