Skip to content

Stop leaking GITHUB_TOKEN to redirect targets in the skill installer - #225

Open
Chirag6722 wants to merge 1 commit into
composio-community:masterfrom
Chirag6722:fix/github-token-leak-on-redirect
Open

Stop leaking GITHUB_TOKEN to redirect targets in the skill installer#225
Chirag6722 wants to merge 1 commit into
composio-community:masterfrom
Chirag6722:fix/github-token-leak-on-redirect

Conversation

@Chirag6722

Copy link
Copy Markdown

Closes #224

The bug

skill-installer/scripts/github_utils.py attached the user's token to the request and then let urlopen follow redirects. urllib's HTTPRedirectHandler copies every header except Content-Length/Content-Type onto the redirect request, so Authorization travelled to whatever host the server pointed at — a different domain, or a downgrade to plain http://.

Reproduced with two local servers, the first redirecting to the second:

Header seen by the *redirect target* host: {'auth': 'token ghp_SECRET_TOKEN', ...}

Both entry points are reachable with user-supplied arguments (--repo, --url, --ref, --path), and the README plus many catalog entries tell users to run the installer against third-party repositories. A redirect anywhere on that path silently exfiltrates a token that usually carries repo scope, while the install still succeeds.

The fix

Treat authentication as a property of the destination, re-evaluated on every hop:

  • is_trusted_url() — the token is attached only when the URL is https and the host is github.com or githubusercontent.com, or a subdomain. GitHub archive downloads redirect to codeload.github.com / objects.githubusercontent.com, so private-repo installs keep working; a look-alike such as github.com.evil.com does not match.
  • _AuthScopingRedirectHandler strips Authorization (from both headers and unredirected_hdrs) whenever a redirect target fails that check. Requests now go through an opener built with it rather than the module-level urlopen.
  • Non-https URLs are refused outright instead of sending credentials in cleartext, and a 30s timeout replaces the previous unbounded wait.
  • Owner, repo, ref and path are percent-encoded when building GitHub URLs, and refs with empty/./.. segments are rejected. Slashes are preserved in ref names, so --ref refs/heads/master still works.

A non-GitHub URL is still fetched — just anonymously — so nothing that worked without a token breaks.

Tests

tests/test_github_utils.py, 24 cases: the host/scheme matrix (including the github.com.evil.com look-alike and host casing), redirect stripping in both directions, which header is sent for GITHUB_TOKEN / GH_TOKEN / neither / untrusted host, non-https refusal, URL encoding, and ref validation — plus an end-to-end redirect chain over real sockets asserting the second host receives no Authorization. That last test fails against the previous implementation:

$ python -m pytest tests/test_github_utils.py -q
24 passed in 1.13s

# same test, with the pre-fix opener restored:
E       AssertionError: assert 'token SECRET' is None

Manually verified against the real repo with no token set: --repo/--path install, --url tree-URL install, --ref refs/heads/master, and list-curated-skills.py all still succeed.

Note on overlap

This branch adds tests/ and .gitignore, which #223 also adds (identical .gitignore, different test file). Whichever merges first, the other should merge cleanly; #223 is what wires tests/ into CI.

🤖 Generated with Claude Code

github_request() set an Authorization header on the request and handed it
to urlopen, which follows redirects. urllib's redirect handler copies every
header except Content-Length and Content-Type onto the new request, so a
redirect served on an install URL received the user's GitHub token - any
host, including a plain-http downgrade. Both installer entry points take
the repo, ref and path from user-supplied arguments and the README invites
running them against third-party repositories, so the leak was reachable
by following an install one-liner from someone else's README.

Scope the credential to the destination instead of the request:

- Only attach the token when the URL is https and the host is github.com
  or githubusercontent.com (or a subdomain), so archive downloads that
  redirect to codeload/objects still authenticate for private repos.
- Re-check on every hop through a redirect handler that strips
  Authorization when the target fails that test, and open requests through
  an opener built with it.
- Refuse non-https URLs outright rather than sending credentials in
  cleartext, and apply a 30s timeout so a hung host cannot block forever.
- Percent-encode owner, repo, ref and path when building GitHub URLs, and
  reject refs containing empty or dot segments.

tests/test_github_utils.py covers the host and scheme rules, which header
is sent where, and an end-to-end redirect chain over real sockets that
asserts the second host sees no Authorization header. That test fails
against the previous implementation.

Closes composio-community#224

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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: skill installer leaks GITHUB_TOKEN to any redirect target

1 participant