132 lines
3.8 KiB
Go
132 lines
3.8 KiB
Go
// SPDX-License-Identifier: 0BSD
|
|
|
|
package views
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"html/template"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gamertan.com/sandwich-hime/sando"
|
|
)
|
|
|
|
func TestBrowseResultsEscapesUntrustedValues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
component := BrowseResults(BrowseView{
|
|
Query: `"><script>alert(1)</script>`,
|
|
Records: []RecordView{{
|
|
URL: `/item?q=" onclick="alert(1)`,
|
|
Title: `<img src=x onerror=alert(1)>`,
|
|
Kind: `" aria-label="injected`,
|
|
}},
|
|
})
|
|
|
|
var output bytes.Buffer
|
|
if err := sando.Render(context.Background(), &output, component); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := output.String()
|
|
if strings.Contains(got, "<script>") || strings.Contains(got, "<img") || strings.Contains(got, `" onclick=`) {
|
|
t.Fatalf("untrusted markup survived escaping: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestBrowseResultsRejectsDangerousURL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
component := BrowseResults(BrowseView{Records: []RecordView{{
|
|
URL: " JaVaScRiPt:\talert(1)",
|
|
Title: "unsafe",
|
|
}}})
|
|
var output bytes.Buffer
|
|
err := sando.Render(context.Background(), &output, component)
|
|
if !errors.Is(err, sando.ErrUnsafeURL) {
|
|
t.Fatalf("expected unsafe URL error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGeneratedComponentPropagatesWriterFailures(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
component := Badge("proof")
|
|
if err := sando.Render(context.Background(), shortWriter{}, component); !errors.Is(err, io.ErrShortWrite) {
|
|
t.Fatalf("short writer error = %v, want io.ErrShortWrite", err)
|
|
}
|
|
|
|
want := errors.New("connection closed")
|
|
if err := sando.Render(context.Background(), errorWriter{err: want}, component); !errors.Is(err, want) {
|
|
t.Fatalf("writer error = %v, want %v", err, want)
|
|
}
|
|
}
|
|
|
|
func TestContextEscapingAgainstHTMLTemplateBaseline(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
baseline := template.Must(template.New("probe").Parse(`<a href="{{ .URL }}" title="{{ .Title }}">{{ .Text }}</a>`))
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
title string
|
|
text string
|
|
}{
|
|
{name: "entities", url: "/items?q=one&kind=book", title: `say "hello" & goodbye`, text: `<strong>not markup</strong>`},
|
|
{name: "unicode", url: "/items/cole", title: "Café & 雪", text: "Cole's archive ♥"},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
data := struct{ URL, Title, Text string }{test.url, test.title, test.text}
|
|
var legacy bytes.Buffer
|
|
if err := baseline.Execute(&legacy, data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var generated bytes.Buffer
|
|
if err := sando.Render(context.Background(), &generated, SecurityProbe(test.url, test.title, test.text)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := strings.TrimSpace(generated.String()), legacy.String(); got != want {
|
|
t.Fatalf("generated escaping differs from html/template baseline\n got: %s\nwant: %s", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDangerousURLIsBlockedAtLeastAsStrictlyAsHTMLTemplate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dangerous = " JaVaScRiPt:\talert(1)"
|
|
baseline := template.Must(template.New("probe").Parse(`<a href="{{ . }}">link</a>`))
|
|
var legacy bytes.Buffer
|
|
if err := baseline.Execute(&legacy, dangerous); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(legacy.String(), "#ZgotmplZ") {
|
|
t.Fatalf("html/template baseline unexpectedly accepted dangerous URL: %s", legacy.String())
|
|
}
|
|
|
|
var generated bytes.Buffer
|
|
err := sando.Render(context.Background(), &generated, SecurityProbe(dangerous, "title", "link"))
|
|
if !errors.Is(err, sando.ErrUnsafeURL) {
|
|
t.Fatalf("generated renderer error = %v, want ErrUnsafeURL", err)
|
|
}
|
|
}
|
|
|
|
type shortWriter struct{}
|
|
|
|
func (shortWriter) Write(value []byte) (int, error) {
|
|
if len(value) == 0 {
|
|
return 0, nil
|
|
}
|
|
return len(value) - 1, nil
|
|
}
|
|
|
|
type errorWriter struct{ err error }
|
|
|
|
func (writer errorWriter) Write([]byte) (int, error) { return 0, writer.err }
|