Skip to content

Sandbox upstream content on package routes - #482

Merged
andrew merged 2 commits into
git-pkgs:mainfrom
simonchrz:fix/sandbox-upstream-content
Oct 15, 2026
Merged

andrew merged 2 commits into
git-pkgs:mainfrom
simonchrz:fix/sandbox-upstream-content

Conversation

@simonchrz

@simonchrz simonchrz commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Package routes share an origin with /ui, and their responses carry bodies and Content-Type values the proxy doesn't control.

Problem

serveArtifactResponse sets Content-Type from the artifact's recorded media type. That type comes from the upstream response, or from whoever pushed a hosted package. Relayed and cached metadata responses also pass upstream headers through. Neither sends X-Content-Type-Options or a Content-Security-Policy.

So an artifact served as text/html or image/svg+xml runs its scripts on the proxy's origin with access to /ui. Sources include a generic upstream, a private registry, a hosted push, or a block page cached as an artifact. Browsers may also sniff an untyped body into HTML.

browse.go already sends Content-Security-Policy: sandbox and X-Content-Type-Options: nosniff for archive files viewed from /ui. This applies the same two headers to the package routes.

Changes

  • New middleware: sandboxUpstreamContent sets X-Content-Type-Options: nosniff and Content-Security-Policy: sandbox on every package-route response, mounted alongside rejectQueryInPath. That covers cached and streamed artifacts, cached metadata, relayed upstream responses and errors.
  • Effect: content that a browser does render runs with scripts disabled and an opaque origin.
  • No client impact: package managers ignore both headers.

Tests

TestPackageRoutesSandboxUpstreamContent mounts the real package routes against a test upstream that serves <script> as text/html. It checks both headers on:

  • a generic artifact, on the first fetch and from the cache;
  • a relayed APK file;
  • an error response.

It failed on all three before the change, and fails again with the middleware removed.

go test -race ./... and golangci-lint v2.13.1 pass.

Also in this PR

Give the detached fetch test time on slow runners fixes a flaky test that's unrelated to the change above. TestServerKeepsFetchAfterClientHangsUp failed once on Windows CI here: the follow-up request timed out after 5 s, in a run where the whole test took 13 s. It passes locally under -race -cpu 1 30 times out of 30. The test checks the outcome (200, body, one upstream fetch), not the speed, so the two 5 s budgets are now 30 s.

Package routes share an origin with /ui, and serve bodies whose
Content-Type comes from upstreams or from whoever pushed a hosted package.
An artifact served as text/html or image/svg+xml ran its scripts with
access to the UI.

- Send X-Content-Type-Options: nosniff and Content-Security-Policy: sandbox
  on every package-route response: cached and streamed artifacts, cached
  metadata, relayed upstream responses and errors
- Archive files browsed from /ui already get the same two headers
TestServerKeepsFetchAfterClientHangsUp gave the upstream fetch 5s to start
and the follow-up request 5s to be answered. On a Windows CI runner, where
the whole test took 13s, the follow-up timed out awaiting headers with
nothing wrong. The test checks the outcome, not the speed, so allow 30s
for both. A real hang still fails.

@andrew andrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Covering every package route in one middleware is the right place for this, and relayed upstream CSPs only add to sandbox rather than replacing it.

@andrew
andrew merged commit 88cf5be into git-pkgs:main Oct 15, 2026
6 checks passed
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.

2 participants