Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/server/untrusted_content_test.go
Simon Chrzanowski cf7eafbbe8 Sandbox upstream content on package routes
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
2026-10-14 17:53:48 +02:00

68 lines
2.6 KiB
Go

package server
import (
"context"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"github.com/git-pkgs/proxy/internal/config"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/handler"
"github.com/git-pkgs/proxy/internal/storage"
"github.com/git-pkgs/registries/fetch"
"github.com/go-chi/chi/v5"
)
// TestPackageRoutesSandboxUpstreamContent checks that responses on package
// routes cannot run as pages on the proxy's origin, which also serves /ui.
// Their bodies and Content-Type come from upstreams, or from whoever pushed a
// hosted package, so an archive served as text/html would otherwise run its
// scripts with access to the UI.
func TestPackageRoutesSandboxUpstreamContent(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = io.WriteString(w, "<script>alert(document.domain)</script>")
}))
t.Cleanup(upstream.Close)
db, err := database.Create(t.TempDir() + "/test.db")
if err != nil {
t.Fatalf("creating database: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
store, err := storage.OpenBucket(context.Background(), "file://"+t.TempDir())
if err != nil {
t.Fatalf("opening storage: %v", err)
}
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(upstream.Client()), fetch.WithMaxRetries(0))
t.Cleanup(func() { _ = fetcher.Close() })
proxy := handler.NewProxy(db, store, fetcher, fetch.NewResolver(), slog.New(slog.NewTextHandler(io.Discard, nil)))
proxy.HTTPClient = upstream.Client()
cfg := config.Default()
cfg.BaseURL = "http://proxy.example"
cfg.Upstream.Generic = map[string]string{"site": upstream.URL}
cfg.Upstream.APK = map[string]string{"alpine": upstream.URL}
s := &Server{cfg: cfg}
r := chi.NewRouter()
s.mountProtocolHandlers(r, proxy)
for _, target := range []string{
"/generic/site/files/page-1.0.html", // cached artifact
"/generic/site/files/page-1.0.html", // the same, from the cache
"/apk/alpine/v3.22/main/x86_64/README.html", // relayed as is
"/npm/does-not-exist%2F..", // an error response
} {
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, target, nil))
if got := w.Header().Get("X-Content-Type-Options"); got != "nosniff" {
t.Errorf("%s (status %d): X-Content-Type-Options = %q, want nosniff", target, w.Code, got)
}
if got := w.Header().Get("Content-Security-Policy"); got != "sandbox" {
t.Errorf("%s (status %d): Content-Security-Policy = %q, want sandbox", target, w.Code, got)
}
}
}