Security: skill installer leaks GITHUB_TOKEN to any redirect target
Problem
skill-installer/scripts/github_utils.py attaches the user's GitHub token to every request and then hands the request to urllib.request.urlopen, which follows redirects automatically:
def github_request(url: str, user_agent: str) -> bytes:
headers = {"User-Agent": user_agent}
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
if token:
headers["Authorization"] = f"token {token}"
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req) as resp: # follows redirects
return resp.read()urllib's HTTPRedirectHandler.redirect_request copies every header from the original request onto the redirect request, stripping only Content-Length and Content-Type. Authorization is carried across the redirect, including to a different host and including a downgrade to plain http://.
Reproduction
Two local servers; the first redirects to the second:
$ python repro.py
Header seen by the *redirect target* host: {'auth': 'token ghp_SECRET_TOKEN', 'host': '127.0.0.1:53585'}The token was minted for github.com and is sent verbatim to an unrelated host.
Why this is reachable
Both installer entry points route through this helper:
install-skill-from-github.pyfetcheshttps://codeload.github.com/{owner}/{repo}/zip/{ref}andhttps://api.github.com/repos/{owner}/{repo}, withowner,repoandreftaken from user-supplied--repo/--url/--ref.list-curated-skills.pyfetcheshttps://api.github.com/repos/{repo}/contents/{path}?ref={ref}from--repo/--path/--ref.
The README tells users to run these scripts against arbitrary third-party repos, and the catalog itself lists installer one-liners pointing at other people's repositories. A user follows an install command from a README or a skill listing, and any redirect served on that path — an attacker-controlled host, a shortener, a compromised or moved endpoint — receives a token that typically carries repo scope. The token is exfiltrated silently; the install still succeeds, so nothing looks wrong.
There is also no scheme check: github_request will happily send the Authorization header over http://, in cleartext.
Root cause
Authentication is treated as a property of the request object rather than of the destination host. Once the header is set, urllib propagates it wherever the server points, and nothing in the helper re-evaluates whether the new destination is entitled to the credential.
Proposed solution
Scope the credential to trusted GitHub hosts and re-check on every hop:
- Send the
Authorizationheader only when the target host is GitHub (github.com,githubusercontent.com, and their subdomains) and the scheme ishttps. GitHub's archive downloads legitimately redirect tocodeload.github.com/objects.githubusercontent.com, so private-repo installs keep working. - Install a custom
HTTPRedirectHandlerthat stripsAuthorizationwhenever the redirect target fails that check, and use an opener built with it instead of the module-levelurlopen. - Reject non-
httpsURLs outright rather than sending credentials in cleartext. - Add a request timeout — the current call can hang indefinitely on an unresponsive host.
- Cover it with tests: a local redirect chain asserting the second host receives no
Authorizationheader, plus unit tests for the host/scheme rules.
Happy to open a PR.
Source: composio-community/awesome-codex-skills