Skip to content
quarry Artifact [0f4f89ca07]
Login

Artifact [0f4f89ca07]

Artifact 0f4f89ca07e3100294ece88327ffda043595b81a0f457be0dca818f8e0492bb2:


package web

import (
	"html/template"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

// Datastar expressions are not sanitized by CSP mode — the docs are
// blunt that a nonce authorises a script, it does not clean it. What
// actually protects these attributes is html/template's contextual
// escaping: it strips a "data-" prefix before classifying an attribute,
// so data-on:drop reads as an on* handler and gets the JAVASCRIPT
// escaper rather than the HTML one.
//
// The distinction matters and is easy to lose. HTML-escaping a quote
// produces ', which the parser decodes back to ' before any script
// reads the attribute — so an HTML-escaped value would still break out
// of a JS string. JS-escaping produces ', which does not.
//
// This is pinned because the values reaching these expressions include
// a board column's status, which comes from .quarry/tickets.json — and
// for a MIRRORED repository that file is upstream content quarry does
// not control.
func TestExpressionsGetJavaScriptEscaping(t *testing.T) {
	tmpl := template.Must(template.New("x").Parse(
		`<div data-on:drop__prevent="@post('/r/repo/ticket/move?to={{.Status}}')"></div>`))
	var b strings.Builder
	hostile := struct{ Status string }{Status: `x') + alert(document.cookie) + ('`}
	if err := tmpl.Execute(&b, hostile); err != nil {
		t.Fatal(err)
	}
	out := b.String()
	if strings.Contains(out, "') + alert(") {
		t.Fatalf("a hostile status broke out of the expression:\n%s", out)
	}
	if !strings.Contains(out, `'`) {
		t.Errorf("the quote was not JS-escaped; HTML escaping decodes back to a quote:\n%s", out)
	}
}

// The policy needs neither of the two escapes that make CSP decorative.
func TestPolicyHasNoUnsafeEscapes(t *testing.T) {
	h := cspHeader("TESTNONCE")
	for _, forbidden := range []string{"unsafe-eval", "unsafe-inline'; script-src", "'unsafe-inline' 'nonce"} {
		if strings.Contains(h, forbidden) {
			t.Errorf("policy contains %q: %s", forbidden, h)
		}
	}
	if !strings.Contains(h, "script-src 'self' 'nonce-TESTNONCE'") {
		t.Errorf("script-src does not carry the nonce: %s", h)
	}
	for _, want := range []string{"default-src 'self'", "frame-ancestors 'none'", "base-uri 'none'"} {
		if !strings.Contains(h, want) {
			t.Errorf("policy is missing %q", want)
		}
	}
}

// A nonce that repeats is one an injected tag can replay.
func TestNonceIsFreshPerResponse(t *testing.T) {
	seen := map[string]bool{}
	h := WithCSP(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {}))
	for i := 0; i < 8; i++ {
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
		got := rec.Header().Get("Content-Security-Policy")
		if got == "" {
			t.Fatal("no policy sent")
		}
		if seen[got] {
			t.Fatal("a nonce repeated across responses")
		}
		seen[got] = true
	}
}

// No inline handler may come back: they are the one thing a strict
// policy cannot allow, and adding one is a single attribute.
func TestNoInlineEventHandlers(t *testing.T) {
	for _, attr := range []string{
		` onclick="`, ` onchange="`, ` onfocus="`, ` onsubmit="`,
		` oninput="`, ` onload="`, ` onerror="`,
	} {
		if strings.Contains(baseHTML, attr) {
			t.Errorf("an inline handler is back: %s", strings.TrimSpace(attr))
		}
	}
}

// The nonce must survive an HTML attribute unchanged.
//
// Standard base64 yields '+', which html/template escapes to &#43;
// inside data-nonce. The browser decodes it before datastar reads it,
// but the two values then disagree by one round of encoding and every
// script datastar injects is rejected — SILENTLY, with no console
// violation. The page simply stops being interactive: sentinels never
// fire, selects never save, and nothing says why.
func TestNonceNeedsNoHTMLEscaping(t *testing.T) {
	for i := 0; i < 200; i++ {
		n := newNonce()
		if n == "" {
			t.Fatal("empty nonce")
		}
		var b strings.Builder
		template.Must(template.New("x").Parse(`<html data-nonce="{{.}}">`)).Execute(&b, n)
		if !strings.Contains(b.String(), `data-nonce="`+n+`"`) {
			t.Fatalf("nonce %q was altered by attribute escaping: %s", n, b.String())
		}
	}
}