- models.AllowedFrontendURL: empty, or an absolute http(s) URL with a host and without whitespace, control characters, a double quote, <, > or & - classes.Rewrite leaves the document unchanged for a refused value, inserts through EscapeText/EscapeAttr and keeps a loc or href that would not stay an http(s) URL - TestRewriteUnsafeFrontendURL, TestAllowedFrontendURL and a stored-row check in TestSitemapImportSmoke
132 lines
6.1 KiB
Go
132 lines
6.1 KiB
Go
package classes_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
|
|
)
|
|
|
|
// rewriteBackend and rewriteDoc are the backend URL and the document of the
|
|
// rewrite tests: a stylesheet instruction, a loc, hreflang links and the
|
|
// backend URL in places the rewrite leaves alone.
|
|
const rewriteBackend = "https://api.ex.pl"
|
|
|
|
var rewriteDoc = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
|
`<?xml-stylesheet type="text/xsl" href="https://api.ex.pl/sitemap.xsl"?>` + "\n" +
|
|
`<urlset><url><loc>https://api.ex.pl/a</loc><lastmod>2026</lastmod><changefreq>https://api.ex.pl</changefreq><priority>1</priority>` +
|
|
`<xhtml:link rel="alternate" hreflang="pl" href="https://api.ex.pl/a"/><xhtml:link rel="alternate" hreflang="en" href="https://api.ex.pl/en/a"/>` +
|
|
`<extra>https://api.ex.pl/x</extra></url><url><loc>https://other.pl/https://api.ex.pl</loc><changefreq>c</changefreq><priority>p</priority></url></urlset>` + "\n"
|
|
|
|
// TestRewrite pins the PHP routes.php frontend_url rewrite: the backend URL
|
|
// is replaced inside loc, inside xhtml:link href and in the exact stylesheet
|
|
// href, nowhere else; slashes are trimmed on both; an empty or "0" frontend
|
|
// URL and equal URLs leave the document unchanged.
|
|
func TestRewrite(t *testing.T) {
|
|
const backend = rewriteBackend
|
|
doc := rewriteDoc
|
|
want := "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
|
`<?xml-stylesheet type="text/xsl" href="https://www.ex.pl/sitemap.xsl"?>` + "\n" +
|
|
`<urlset><url><loc>https://www.ex.pl/a</loc><lastmod>2026</lastmod><changefreq>https://api.ex.pl</changefreq><priority>1</priority>` +
|
|
`<xhtml:link rel="alternate" hreflang="pl" href="https://www.ex.pl/a"/><xhtml:link rel="alternate" hreflang="en" href="https://www.ex.pl/en/a"/>` +
|
|
`<extra>https://api.ex.pl/x</extra></url><url><loc>https://other.pl/https://www.ex.pl</loc><changefreq>c</changefreq><priority>p</priority></url></urlset>` + "\n"
|
|
for _, frontend := range []string{"https://www.ex.pl", "https://www.ex.pl/", "https://www.ex.pl///"} {
|
|
for _, back := range []string{backend, backend + "/"} {
|
|
if got := classes.Rewrite(doc, back, frontend); got != want {
|
|
t.Fatalf("Rewrite(%q, %q):\n%s\nwant:\n%s", back, frontend, got, want)
|
|
}
|
|
}
|
|
}
|
|
for _, frontend := range []string{"", "0"} {
|
|
if got := classes.Rewrite(doc, backend, frontend); got != doc {
|
|
t.Fatalf("frontend %q changed the document", frontend)
|
|
}
|
|
}
|
|
if got := classes.Rewrite(doc, backend, backend); got != doc {
|
|
t.Fatal("equal URLs changed the document")
|
|
}
|
|
if got := classes.Rewrite(doc, backend+"/", backend); got != doc {
|
|
t.Fatal("equal URLs up to a slash changed the document")
|
|
}
|
|
// An empty backend URL replaces nothing.
|
|
if got := classes.Rewrite(doc, "", "https://www.ex.pl"); got != doc {
|
|
t.Fatal("an empty backend URL changed the document")
|
|
}
|
|
// A stylesheet on another path is not the PHP pattern.
|
|
other := `<?xml-stylesheet type="text/xsl" href="https://api.ex.pl/other.xsl"?>`
|
|
if got := classes.Rewrite(other, backend, "https://www.ex.pl"); got != other {
|
|
t.Fatalf("other stylesheet = %q", got)
|
|
}
|
|
}
|
|
|
|
// The two frontend_url values of the T-17-15 reproduction: a script URL the
|
|
// stylesheet would render as a link, and a value that closes the attribute
|
|
// and the element it is written into.
|
|
var frontendAttacks = []string{
|
|
"javascript:alert(document.domain)//",
|
|
`https://x"/><y a="`,
|
|
}
|
|
|
|
// TestRewriteUnsafeFrontendURL pins the T-17-15 output rule: a frontend URL
|
|
// that models.AllowedFrontendURL refuses leaves the document byte for byte
|
|
// as it was, so no part of it reaches loc, an hreflang href or the
|
|
// stylesheet instruction; an accepted one still rewrites; and a loc or href
|
|
// that the rewrite would turn into something other than an http(s) URL
|
|
// keeps its value.
|
|
func TestRewriteUnsafeFrontendURL(t *testing.T) {
|
|
refused := append([]string{
|
|
"https://www.ex.pl/?a=1&b=2",
|
|
"https://www.ex.pl <x>",
|
|
" https://www.ex.pl",
|
|
"https://www.ex.pl\n",
|
|
"//www.ex.pl",
|
|
"www.ex.pl",
|
|
"https:///x",
|
|
"data:text/html,x",
|
|
}, frontendAttacks...)
|
|
for _, frontend := range refused {
|
|
if got := classes.Rewrite(rewriteDoc, rewriteBackend, frontend); got != rewriteDoc {
|
|
t.Errorf("frontend %q changed the document:\n%s", frontend, got)
|
|
}
|
|
}
|
|
|
|
// The reproduction through the generator.
|
|
const root = "https://api.example.test"
|
|
xml, err := (&classes.Generator{Root: root}).Generate(context.Background(), urlItems("/blog/x"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(xml, `href="https://api.example.test/sitemap.xsl"`) {
|
|
t.Fatalf("the generated document lacks the stylesheet instruction:\n%s", xml)
|
|
}
|
|
for _, frontend := range frontendAttacks {
|
|
got := classes.Rewrite(xml, classes.BackendURL(root), frontend)
|
|
if got != xml {
|
|
t.Errorf("frontend %q changed the generated sitemap:\n%s", frontend, got)
|
|
}
|
|
if !strings.Contains(got, "<loc>https://api.example.test/blog/x</loc>") {
|
|
t.Errorf("frontend %q: the loc is not on the backend URL:\n%s", frontend, got)
|
|
}
|
|
if lower := strings.ToLower(got); strings.Contains(lower, "javascript") || strings.Contains(lower, "<y a=") {
|
|
t.Errorf("frontend %q reached the sitemap:\n%s", frontend, got)
|
|
}
|
|
}
|
|
|
|
// A value the predicate accepts still rewrites all three places.
|
|
got := classes.Rewrite(xml, classes.BackendURL(root), "https://www.example.test")
|
|
if !strings.Contains(got, "<loc>https://www.example.test/blog/x</loc>") || !strings.Contains(got, `href="https://www.example.test/sitemap.xsl"`) || strings.Contains(got, "api.example.test") {
|
|
t.Errorf("a valid frontend URL did not rewrite the sitemap:\n%s", got)
|
|
}
|
|
if got := classes.Rewrite(xml, classes.BackendURL(root), "HTTPS://WWW.EXAMPLE.TEST"); !strings.Contains(got, "<loc>HTTPS://WWW.EXAMPLE.TEST/blog/x</loc>") {
|
|
t.Errorf("an upper-case frontend URL did not rewrite the loc:\n%s", got)
|
|
}
|
|
|
|
// The http(s) output filter after the rewrite: replacing "s://api.ex.pl"
|
|
// would write httphttps://www.ex.pl/a, so loc and href keep their values.
|
|
entry := `<url><loc>https://api.ex.pl/a</loc><xhtml:link rel="alternate" hreflang="pl" href="https://api.ex.pl/a"/></url>`
|
|
if got := classes.Rewrite(entry, "s://api.ex.pl", "https://www.ex.pl"); got != entry {
|
|
t.Errorf("a rewrite to a non-http(s) URL was written:\n%s", got)
|
|
}
|
|
}
|