Skip to content
quarry Artifact [02a2c29d0f]
Login

Artifact [02a2c29d0f]

Artifact 02a2c29d0f4666bd63a3bd7cdb5f88fb5299491f82cdf5648d263e3add096770:


package imgproxy

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"
)

var png1x1 = []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89")

func upstream(t *testing.T, ctype string, body []byte) (*httptest.Server, *int) {
	t.Helper()
	hits := 0
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		hits++
		w.Header().Set("Content-Type", ctype)
		w.Write(body)
	}))
	t.Cleanup(srv.Close)
	return srv, &hits
}

func get(t *testing.T, p *Proxy, path string) *httptest.ResponseRecorder {
	t.Helper()
	rec := httptest.NewRecorder()
	p.ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
	return rec
}

// Only what the renderer signed is fetched: a URL with no signature, a
// wrong one, or one signed for a different image is refused.
func TestUnsignedImagesAreRefused(t *testing.T) {
	p, err := New(t.TempDir(), true)
	if err != nil {
		t.Fatal(err)
	}
	src, hits := upstream(t, "image/png", png1x1)
	for _, path := range []string{
		"/img?u=" + url.QueryEscape(src.URL+"/a.png"),
		"/img?u=" + url.QueryEscape(src.URL+"/a.png") + "&s=forged",
		strings.Replace(p.URL(src.URL+"/b.png"), url.QueryEscape("/b.png"), url.QueryEscape("/a.png"), 1),
	} {
		if rec := get(t, p, path); rec.Code != http.StatusForbidden {
			t.Errorf("%s: %d, want 403", path, rec.Code)
		}
	}
	if *hits != 0 {
		t.Fatalf("a refused request reached the upstream %d times", *hits)
	}
}

// The guard runs at connect time: a correctly signed URL that points at a
// loopback address is still not fetched.
func TestPrivateAddressesAreNotFetched(t *testing.T) {
	p, err := New(t.TempDir(), false)
	if err != nil {
		t.Fatal(err)
	}
	src, hits := upstream(t, "image/png", png1x1)
	if rec := get(t, p, p.URL(src.URL+"/a.png")); rec.Code != http.StatusBadGateway {
		t.Fatalf("a loopback image answered %d", rec.Code)
	}
	if *hits != 0 {
		t.Fatal("the guard let a loopback fetch through")
	}
}

// An image is served with the headers that keep it an image, and cached:
// the second request does not reach the upstream, and a stale copy is
// served when the upstream has gone.
func TestImagesAreServedAndCached(t *testing.T) {
	p, err := New(t.TempDir(), true)
	if err != nil {
		t.Fatal(err)
	}
	src, hits := upstream(t, "image/png", png1x1)
	path := p.URL(src.URL + "/badge.png?branch=main")
	rec := get(t, p, path)
	if rec.Code != http.StatusOK || rec.Body.String() != string(png1x1) {
		t.Fatalf("first request: %d", rec.Code)
	}
	if rec.Header().Get("Content-Type") != "image/png" || rec.Header().Get("X-Content-Type-Options") != "nosniff" {
		t.Errorf("headers: %v", rec.Header())
	}
	get(t, p, path)
	if *hits != 1 {
		t.Fatalf("a fresh cached image was fetched again (%d hits)", *hits)
	}
	p.now = func() time.Time { return time.Now().Add(2 * TTL) }
	src.Close()
	if rec := get(t, p, path); rec.Code != http.StatusOK {
		t.Fatalf("a stale copy was not served when the upstream was down: %d", rec.Code)
	}
}

// Anything that is not an image is refused, and so is an image past the
// size cap.
func TestNonImagesAndOversizeAreRefused(t *testing.T) {
	p, err := New(t.TempDir(), true)
	if err != nil {
		t.Fatal(err)
	}
	html, _ := upstream(t, "text/html", []byte("<script>alert(1)</script>"))
	if rec := get(t, p, p.URL(html.URL+"/x")); rec.Code != http.StatusBadGateway {
		t.Errorf("html answered %d", rec.Code)
	}
	big, _ := upstream(t, "image/png", make([]byte, MaxBytes+1))
	if rec := get(t, p, p.URL(big.URL+"/big.png")); rec.Code != http.StatusBadGateway {
		t.Errorf("an oversize image answered %d", rec.Code)
	}
}

// SVG can carry script; served here it is sandboxed.
func TestSVGIsSandboxed(t *testing.T) {
	p, err := New(t.TempDir(), true)
	if err != nil {
		t.Fatal(err)
	}
	svg, _ := upstream(t, "image/svg+xml; charset=utf-8", []byte(`<svg xmlns="http://www.w3.org/2000/svg"/>`))
	rec := get(t, p, p.URL(svg.URL+"/badge.svg"))
	if rec.Code != http.StatusOK {
		t.Fatalf("svg: %d", rec.Code)
	}
	if csp := rec.Header().Get("Content-Security-Policy"); !strings.Contains(csp, "sandbox") || !strings.Contains(csp, "default-src 'none'") {
		t.Errorf("svg csp = %q", csp)
	}
}

// The key survives a restart, so a page rendered before it still loads.
func TestTheKeyPersists(t *testing.T) {
	dir := t.TempDir()
	a, err := New(dir, false)
	if err != nil {
		t.Fatal(err)
	}
	b, err := New(dir, false)
	if err != nil {
		t.Fatal(err)
	}
	if a.URL("https://example.org/x.png") != b.URL("https://example.org/x.png") {
		t.Fatal("a second start signs differently")
	}
	if a.URL("javascript:alert(1)") != "" || a.URL("/local.png") != "" {
		t.Fatal("a non-http target was given a proxy URL")
	}
}