package main import ( "net/http/httptest" "strings" "testing" ) // #34: attacker-controlled content_type must not let a paste be served as // HTML/SVG/XML from /raw (stored XSS). Only a fixed safe set passes through. func TestRawRejectsHTMLContentType(t *testing.T) { globalLimiter = newLimiter() // fresh rate-limit buckets s := testServer(t) h := s.routes() for _, ct := range []string{ "text/html", "TEXT/HTML", "text/html;charset=utf-8", "text/html;x=1", "application/xhtml+xml", "image/svg+xml", "text/html,", } { globalLimiter = newLimiter() // burst 5, loop makes 7 creates body := `{"content":"","content_type":"` + ct + `"}` rec := httptest.NewRecorder() req := httptest.NewRequest("POST", "/api/pastes", strings.NewReader(body)) h.ServeHTTP(rec, req) if rec.Code != 201 { t.Fatalf("ct %q: create got %d: %s", ct, rec.Code, rec.Body.String()) } id := jsonField(t, rec.Body.String(), "id") rec = httptest.NewRecorder() req = httptest.NewRequest("GET", "/raw/"+id, nil) h.ServeHTTP(rec, req) if got := rec.Header().Get("Content-Type"); got == ct { t.Errorf("ct %q was served verbatim from /raw (stored XSS vector)", ct) } if got := rec.Header().Get("X-Content-Type-Options"); got != "nosniff" { t.Errorf("ct %q: /raw missing X-Content-Type-Options: nosniff", ct) } } } func TestRawAllowsSafeContentType(t *testing.T) { globalLimiter = newLimiter() s := testServer(t) h := s.routes() for _, ct := range []string{"text/plain", "image/png", "application/pdf", "application/octet-stream"} { globalLimiter = newLimiter() body := `{"content":"hi","content_type":"` + ct + `"}` rec := httptest.NewRecorder() req := httptest.NewRequest("POST", "/api/pastes", strings.NewReader(body)) h.ServeHTTP(rec, req) if rec.Code != 201 { t.Fatalf("ct %q: create got %d", ct, rec.Code) } id := jsonField(t, rec.Body.String(), "id") rec = httptest.NewRecorder() req = httptest.NewRequest("GET", "/raw/"+id, nil) h.ServeHTTP(rec, req) if got := rec.Header().Get("Content-Type"); got != ct { t.Errorf("ct %q: got %q", ct, got) } } }