PostHog / PostHog/posthog

Scout `gh api` reviewer-evidence template interpolates unvalidated repository slugs into a shell literal

Open
#73,691 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
39.9k
Forks
3.4k
Avg merge
6h 51m
Merged PRs (30d)
232

Description

Problem

_GITHUB_EVIDENCE_REPORT in products/signals/backend/scout_harness/prompt.py gives report-channel scouts this template for reviewer evidence:

gh api 'repos/<owner>/<repo>/commits?path=<dir-or-file>&per_page=30' --jq '...'

<owner>/<repo> is filled in by the scout from whatever repository value it is working with. When that value came from a task, it is not trustworthy: TaskWriteSerializer.validate_repository (products/tasks/backend/presentation/serializers.py:610) only checks for two non-empty slash-separated parts and lowercases the result. It does not restrict characters, so quotes, semicolons and $(...) all pass validation and land in Task.repository.

A value shaped like org/';<command>;# therefore terminates the single-quoted literal and runs as a shell command. The sandbox has an authenticated gh token in scope, so the payload can read it.

Reachability

The section is flag-gated per team and only appended for report-channel scouts with a read-only GitHub token, which bounds it. Within that gate the path is ordinary behavior rather than an edge case:

  1. A project member creates a task with a crafted repository.
  2. They generate enough failed runs for it to surface as a candidate in a scout's analysis.
  3. The scout follows its routing guidance, substitutes the repository into the gh api template, and the sandbox shell executes it.

The token is read-only for repository contents, so the direct blast radius is read access plus whatever the exfiltrated token allows — not a write path. Filing as a vulnerability in the harness rather than in any one scout: the template is shared by every report-channel scout, and the weak validator is upstream of all of them.

Surfaced in review of #73685, which added a scout that reads task-derived repository values. That PR hardened its own body (a task-derived repository must never enter a shell command and must match a connected repository before use) and filters on a cityHash64 fingerprint rather than the string in SQL. Those are prompt-level mitigations in one scout; they don't fix the template.

Candidate fixes

  1. Resolve against connected repositories before use. The scout already has the project's integrations; requiring a candidate to match a known-connected slug before it reaches any command turns an arbitrary string into a closed set. Strongest option, and it composes with the others.
  2. Remove the shell quoting problem from the template. Pass the path via gh api --field / argument form rather than embedding it in a single-quoted string, so no interpolation happens inside shell quoting.
  3. Tighten validate_repository to the character set GitHub actually permits for owner and repo names. Cheap, and it shrinks the problem for every consumer of the column, not just this template — though it won't retroactively clean values already stored.

3 is worth doing regardless of what happens to the template.

Notes

Worth auditing whether any other prompt or tool surface interpolates task- or user-derived strings into shell templates the same way. The pattern here is "value that passed a shape check, not a content check, reaches a command line", and validate_repository is unlikely to be the only permissive validator feeding one.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with _GITHUB_EVIDENCE_REPORT in products/signals/backend/scout_harness/prompt.py and then inspect TaskWriteSerializer.validate_repository at products/tasks/backend/presentation/serializers.py:610. Trace how task-derived repository values reach report-channel scout commands, review the mitigations and candidate fixes described here, and verify that untrusted values cannot be executed or exposed through the shared template.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.