Skip to content

[Security] Release transport: credential confinement across origins and redirects (PR-0A) #158

Closed

Description

@Rwanbt

Problem

The release transport sends the provider token to any URL it is asked to fetch, and
urllib forwards Authorization across redirects — including cross-origin ones.
Concretely, today:

  • ReleaseApiProvider._get() attaches GITHUB_TOKEN/GH_TOKEN to every request,
    including an artifact URL (browser_download_url) taken from metadata served by a
    custom AINATIVE_UPDATE_URL. Any custom source can collect the user's GitHub token.
  • urllib's default redirect handler copies all request headers to the redirect
    target. A 302 from an otherwise legitimate endpoint to any other origin leaks
    the bearer token.
  • AINATIVE_UPDATE_URL inherits provider credentials, although it is documented as
    an anonymous custom source.

Source of the fix: Multi-Forge v1.3.2-final §10–14 (PR-0A), ADR-MF-03 addendum.

Expected outcome

Release transport enforces one credential rule: a provider credential is sent only
to the origin it belongs to (auth_origin), never to any other origin, never
across a redirect to another origin, and never for AINATIVE_UPDATE_URL.

Acceptance criteria

  • A private urllib opener replaces urllib's default redirect forwarding; no
    global urllib monkeypatch and no reliance on module-level urlopen.
  • URL validation: HTTPS required; userinfo (user@host) rejected; origins
    compared normalized (scheme://host[:port], default port normalized, case
    normalized).
  • Credential attached iff the request origin equals the endpoint's
    auth_origin; auth_origin is endpoint configuration, never derived from the
    URL being fetched.
  • Metadata policy: Authorization: Bearer + Accept: application/vnd.github+json
    at the trusted GitHub API origin; same-origin metadata redirects followed;
    cross-origin metadata redirects refused.
  • Artifact policy: bounded bytes; 302 to the CDN followed anonymously with
    every credential stripped; HTTPS maintained on every hop; HTTPS → HTTP
    downgrade refused.
  • Arbitrary browser_download_url is never authenticated. GitHub private
    release assets are fetched through the asset API URL (assets[].url) with
    Accept: application/octet-stream and the bearer attached only at the API
    origin.
  • AINATIVE_UPDATE_URL is anonymous only: never GITHUB_TOKEN, GH_TOKEN or
    GITLAB_TOKEN (documented as a breaking behavior change).
  • Redirect-loop bound enforced (explicit maximum hops); userinfo in a redirect
    target rejected.
  • Oversized responses refused before parsing/returning.
  • Characterization tests recorded before the production change, then security
    tests: trusted GitHub API receives bearer; evil initial URL receives no
    bearer; AINATIVE_UPDATE_URL receives no bearer; userinfo URL rejected; HTTP
    initial URL rejected where HTTPS is required; HTTPS→HTTP redirect rejected;
    same-origin metadata redirect followed; cross-origin metadata redirect
    rejected; asset API → CDN strips bearer; redirect loop rejected; oversized
    response rejected.
  • Gate: zero provider credentials observed at non-approved origins across the
    full metadata + artifact flow.
  • Existing lifecycle tests green; GitHub public release behavior preserved;
    GitHub private asset fixture green; custom source behavior documented
    (CHANGELOG security/breaking + update documentation).

Out of scope

  • release-providers.json, named providers, selector conflict policy (PR-4A per
    ADR-MF-03).
  • GitLab provider (PR-5).
  • Universal HTTP client, generic Forge SDK, unrelated provider redesign.
  • Repository-local provider trust configuration.

Context

Dependencies

Activity

  1. added
    type:securitySecurity hardening or threat-model work
    priority:P1Major user-facing defect or release blocker
    on Sep 16, 2026
  2. self-assigned this
    on Sep 16, 2026
  3. Rwanbt commented on Sep 16, 2026

    @Rwanbt
    OwnerAuthor

    Claiming this Issue for implementation.

  4. Rwanbt commented on Sep 16, 2026

    @Rwanbt
    OwnerAuthor

    Reopened: this work was merged into main by mistake. main has been restored to its pre-merge state (9301253); the tested work now lives on dev (merge commit 0721237). The Issue stays open until the final dev -> main promotion, once the whole plan is implemented and tested.

  5. reopened this on Sep 16, 2026
  6. Rwanbt commented on Sep 17, 2026

    @Rwanbt
    OwnerAuthor

    Done: the work is merged into main (squash 8f319ad via PR #176, full CI matrix green including the Production Gate). Closed as completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

priority:P1Major user-facing defect or release blockertype:securitySecurity hardening or threat-model work

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions