Skip to content

security(personas): vet the persona repo URL before cloning it (#521) - #656

Open
harrylettering wants to merge 1 commit into
andrewyng:mainfrom
harrylettering:fix/persona-git-url-validation
Open

security(personas): vet the persona repo URL before cloning it (#521)#656
harrylettering wants to merge 1 commit into
andrewyng:mainfrom
harrylettering:fix/persona-git-url-validation

Conversation

@harrylettering

Copy link
Copy Markdown

Problem

PersonaRegistry.install_from_git passed the caller's string straight to git clone, and git reads parts of that string as instructions rather than as an address:

  • ext::sh -c '<command>' runs <command> through git's remote-helper mechanism. protocol.ext.allow defaults to user, so a direct clone is exactly the case it still permits.
  • A URL starting with - is parsed as an option (--upload-pack=...).

Both execute code before any manifest is parsed, so before the install-time consent screen the persona trust model relies on. Reported as #521.

Fix

validate_git_url vets the address up front:

  • refuses any <helper>:: form (ext::, fd::, …)
  • refuses a leading -
  • refuses whitespace and control characters
  • otherwise requires https://, ssh://, git://, or the scp-like user@host:path

It runs in two places: git_clone (the process boundary) and clone_persona_repo (the logical entry), so a caller that injects its own clone is not a way around the check and a refused URL never reaches the cache-directory naming either.

The clone command gains two more locks:

git -c protocol.ext.allow=never clone --depth 1 -- <url> <dest>

-- ends option parsing; protocol.ext.allow=never is a second lock on the helper path.

/v1/personas/install already surfaces the raised ValueError to the caller, so no server change was needed.

Tests

Four new tests in tests/test_persona_loading.py: six refused URLs, four accepted ones, the injected-clone case proving the vet happens before the clone, and the argv shape.

tests/test_persona_loading.py passes (22). The full suite shows the same 28 failures before and after this change on my machine (Slack relay, browser address guard, UI e2e — all network/Playwright dependent), with no new ones.

Trade-off

Refusing every :: also refuses IPv6 literals such as ssh://[::1]/repo. A loopback IPv6 host is not a plausible persona source, and the refusal names its reason, so this fails closed on purpose.

Fixes #521

🤖 Generated with Claude Code

https://claude.ai/code/session_01HYvy9TcuTD4CZMVcdwwH4D

`install_from_git` handed the caller's string straight to `git clone`, and git
reads parts of that string as instructions rather than as an address: `ext::sh
-c ...` runs a command through the remote-helper mechanism, and a URL starting
with `-` is parsed as an option. Both execute before any manifest is parsed, so
before the consent screen the install flow is supposed to rely on.

`validate_git_url` now refuses any `<helper>::` form, a leading dash, whitespace
and control characters, and anything outside https / ssh / git / `user@host:path`.
It runs at the process boundary in `git_clone` and at the logical entry in
`clone_persona_repo`, so an injected clone is not a way around it. The clone
command itself adds `--` to end option parsing and
`-c protocol.ext.allow=never` as a second lock on the helper path.

Fixes andrewyng#521

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HYvy9TcuTD4CZMVcdwwH4D
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: persona install executes arbitrary commands via unvalidated git_url (ext:: transport / option injection), pre-consent

1 participant