123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 | package markdown
import (
"bytes"
"html"
"net/url"
"path"
"path/filepath"
"regexp"
"strings"
"testing"
"code.bruce-hill.com/repo-renderer/internal/highlight"
)
// Run one with `make fuzz`; plain `go test` runs only the seeds.
var fuzzCtx = Context{Dir: "docs/sub", PageURL: pageURL,
ImageURL: func(p string) string { return "/raw/r/" + escapePath(p) }}
// A README's links come from the pusher. Resolve either leaves one alone
// or points it at a page or file inside the repository, keeping its query
// and fragment.
func FuzzResolve(f *testing.F) {
for _, s := range []string{
"guide.md", "./a.md#top", "../../../x", "/../x", "a%2F..%2F..%2F..%2Fx", "%2e%2e/%2e%2e/%2e%2e/x",
"a?b=1#c", "javascript:alert(1)", "//evil.example/x", "#frag", "", "/", "a\\..\\..\\x", "%zz",
} {
f.Add(s, false)
}
f.Fuzz(func(t *testing.T, dest string, image bool) {
out := Resolve(dest, fuzzCtx, image)
if out == dest {
return
}
prefix := "/code/r"
if image {
prefix = "/raw/r"
}
u, err := url.Parse(out)
if err != nil || u.Scheme != "" || u.Host != "" || !strings.HasPrefix(u.Path, prefix) {
t.Fatalf("Resolve(%q) = %q, not under %s (%v)", dest, out, prefix, err)
}
rel := strings.TrimPrefix(strings.TrimPrefix(u.Path, prefix), "/")
if rel != "" && (path.Clean(rel) != rel || !filepath.IsLocal(rel)) {
t.Fatalf("Resolve(%q) = %q, which names %q", dest, out, rel)
}
if in, err := url.Parse(dest); err == nil && (u.RawQuery != in.RawQuery || u.Fragment != in.Fragment) {
t.Fatalf("Resolve(%q) = %q, changing the query or fragment", dest, out)
}
})
}
// The tags and attributes the renderer itself writes. Anything else in the
// output came from the document and should have been escaped.
var (
allowedTags = map[string]bool{
"a": true, "blockquote": true, "br": true, "code": true, "del": true, "div": true, "em": true,
"h1": true, "h2": true, "h3": true, "h4": true, "h5": true, "h6": true, "hr": true, "img": true,
"input": true, "li": true, "ol": true, "p": true, "pre": true, "span": true, "strong": true,
"sup": true, "table": true, "tbody": true, "td": true, "th": true, "thead": true, "tr": true, "ul": true,
}
allowedAttrs = map[string]bool{
"alt": true, "checked": true, "class": true, "data-lang": true, "disabled": true, "href": true,
"id": true, "role": true, "src": true, "start": true, "style": true, "title": true, "type": true,
}
tagRE = regexp.MustCompile(`<(/?)([^\s>/]*)([^>]*)>?`)
attrRE = regexp.MustCompile(`\s([^\s=>]+)(?:="([^"]*)")?`)
// What a browser ignores in a URL before deciding its scheme.
urlNoise = strings.NewReplacer("\t", "", "\n", "", "\r", "")
)
// Whatever a README contains, its page has only markup the renderer wrote:
// no tag, attribute or link scheme that could run script.
func FuzzRender(f *testing.F) {
hl, err := highlight.New(nil, nil, "github")
if err != nil {
f.Fatal(err)
}
r := New(hl)
for _, s := range []string{
"# Hi\n\n*a* **b** `c` [d](e.md) \n", "<script>alert(1)</script>\n", "a <b onclick=x>c</b>\n",
"[x](javascript:alert(1))\n", "[x]( JaVa\tScRiPt:alert(1))\n", "[x](javascript:alert(1))\n",
"<javascript:alert(1)>\n", "\n", "```html\n<b>\n```\n", "- [ ] a\n- [x] b\n",
"| a | b |\n|---|:-:|\n| c | d |\n", "x[^1]\n\n[^1]: note\n", "% title\n\nbody\n", " indented\n",
"[x]: javascript:alert(1)\n\n[x]\n", "<div>\n*a*\n</div>\n", "~~gone~~ https://example.com\n",
"```\" onmouseover=\"x\n```\n",
} {
f.Add(s)
}
f.Fuzz(func(t *testing.T, src string) {
out, err := r.Render([]byte(src), fuzzCtx)
if err != nil {
return
}
for _, m := range tagRE.FindAllStringSubmatch(string(out), -1) {
if !allowedTags[strings.ToLower(m[2])] {
t.Fatalf("%q renders a <%s%s> tag:\n%s", src, m[1], m[2], out)
}
for _, a := range attrRE.FindAllStringSubmatch(m[3], -1) {
name := strings.ToLower(a[1])
if !allowedAttrs[name] {
t.Fatalf("%q renders a %s attribute:\n%s", src, name, out)
}
if name != "href" && name != "src" {
continue
}
u := strings.ToLower(strings.TrimLeft(urlNoise.Replace(html.UnescapeString(a[2])), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f "))
if strings.HasPrefix(u, "javascript:") || strings.HasPrefix(u, "vbscript:") ||
strings.HasPrefix(u, "data:") && !strings.HasPrefix(u, "data:image/") {
t.Fatalf("%q renders %s=%q:\n%s", src, name, a[2], out)
}
}
}
})
}
// StripPandocMeta only ever drops a prefix.
func FuzzStripPandocMeta(f *testing.F) {
for _, s := range []string{"", "% t\n% a\n\nbody", "%", "% t\n more\n\tmore\nbody", "%t", "x\n% t\n"} {
f.Add([]byte(s))
}
f.Fuzz(func(t *testing.T, src []byte) {
out := StripPandocMeta(src)
if !bytes.HasSuffix(src, out) {
t.Fatalf("StripPandocMeta(%q) = %q, not a suffix", src, out)
}
if len(src) > 0 && src[0] != '%' && len(out) != len(src) {
t.Fatalf("StripPandocMeta(%q) = %q without a title block", src, out)
}
})
}
|