fix(17): CR-01 refuse non-http sitemap URLs on save, import and output
A url item stored as javascript://%0aalert(1)%0a//x passed Url::to and reached <loc>, which sitemap.xsl renders as a link on the site origin. - The definitions form refuses, per row, a url that is not a path or an http(s) URL (classes.AllowedItemURL) and a urlPattern that is not a path (signpost.ValidItemPattern), keyed data.<i>.url / .urlPattern. - Generator.addItem drops a loc or alternate that is not http(s). - sitemap:import-winter drops such url items and clears such patterns with a report line; sitemap:import refuses such an entry.
This commit is contained in:
@@ -196,7 +196,8 @@ func (g *Generator) manifest() Manifest {
|
||||
// missing URL filled through Url::to, lastmod from the record or the
|
||||
// manifest, BeforeAddItem, an empty URL dropped and the URL passed through
|
||||
// Url::to again, the count, the tier or item priority, the elements and
|
||||
// AddItem.
|
||||
// AddItem. Unlike PHP, a URL or alternate that does not come out as an
|
||||
// http or https URL is dropped.
|
||||
func (g *Generator) addItem(ctx context.Context, item *models.DefinitionItem, info itemInfo) error {
|
||||
if g.count >= MaxURLs {
|
||||
return nil
|
||||
@@ -229,6 +230,12 @@ func (g *Generator) addItem(ctx context.Context, item *models.DefinitionItem, in
|
||||
return nil
|
||||
}
|
||||
loc := URLTo(g.Root, info.url)
|
||||
if !httpURL(loc) {
|
||||
// Only http and https URLs are written: a javascript: or other
|
||||
// scheme that an import, a registry type or a pattern let through
|
||||
// would be a clickable link in sitemap.xsl.
|
||||
return nil
|
||||
}
|
||||
g.count++
|
||||
priority, ok := g.Tiers.Priority(loc, g.Settings)
|
||||
if !ok {
|
||||
@@ -242,7 +249,11 @@ func (g *Generator) addItem(ctx context.Context, item *models.DefinitionItem, in
|
||||
}
|
||||
if len(info.alternates) > 1 {
|
||||
for _, a := range info.alternates {
|
||||
entry.Alternates = append(entry.Alternates, Alternate{Hreflang: a.Hreflang, Href: URLTo(g.Root, a.Href)})
|
||||
href := URLTo(g.Root, a.Href)
|
||||
if !httpURL(href) {
|
||||
continue
|
||||
}
|
||||
entry.Alternates = append(entry.Alternates, Alternate{Hreflang: a.Hreflang, Href: href})
|
||||
}
|
||||
}
|
||||
if g.Events != nil {
|
||||
|
||||
@@ -149,3 +149,35 @@ func TestURLCap(t *testing.T) {
|
||||
t.Fatalf("no items = %q %v", empty, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnlyHTTPLocs pins the CR-01 output filter: a URL that Url::to keeps
|
||||
// but that is not http or https (a javascript: URL FILTER_VALIDATE_URL
|
||||
// accepts, mailto:, a bare fragment) is not written and not counted. A
|
||||
// protocol-relative URL comes out of Url::to on the root host.
|
||||
func TestOnlyHTTPLocs(t *testing.T) {
|
||||
gen := &classes.Generator{Root: "https://api.example.test"}
|
||||
xml, err := gen.Generate(context.Background(), urlItems(
|
||||
"javascript://%0aalert(1)%0a//x",
|
||||
"JavaScript://example.test/%0aalert(1)",
|
||||
"//evil.test/x",
|
||||
"mailto:a@example.test",
|
||||
"#top",
|
||||
"/ok",
|
||||
"https://other.example.test/page",
|
||||
))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n := strings.Count(xml, "<url>"); n != 3 {
|
||||
t.Fatalf("%d url elements, want 3:\n%s", n, xml)
|
||||
}
|
||||
if !strings.Contains(xml, "<loc>https://api.example.test/evil.test/x</loc>") {
|
||||
t.Fatalf("the protocol-relative URL did not stay on the root host:\n%s", xml)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(xml), "javascript") || strings.Contains(xml, "mailto:") || strings.Contains(xml, "#top") {
|
||||
t.Fatalf("a non-http URL was written:\n%s", xml)
|
||||
}
|
||||
if !strings.Contains(xml, "<loc>https://api.example.test/ok</loc>") || !strings.Contains(xml, "<loc>https://other.example.test/page</loc>") {
|
||||
t.Fatalf("an http(s) URL is missing:\n%s", xml)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,3 +531,36 @@ func URLPath(raw string) string {
|
||||
}
|
||||
return u.path
|
||||
}
|
||||
|
||||
// schemePrefix matches a URL that starts with a scheme and its colon.
|
||||
var schemePrefix = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9+.\-]*:`)
|
||||
|
||||
// AllowedItemURL reports whether a url item's address may be stored: empty,
|
||||
// a path (relative or starting with a single "/"), or an absolute http or
|
||||
// https URL. Any other scheme (javascript:, data:, mailto: and the like), a
|
||||
// protocol-relative "//host" or "/\host", a bare fragment, surrounding
|
||||
// whitespace and control characters are refused. Url::to keeps any URL
|
||||
// FILTER_VALIDATE_URL accepts, so without this a javascript: URL reached
|
||||
// <loc>, which sitemap.xsl renders as a link.
|
||||
func AllowedItemURL(u string) bool {
|
||||
if u == "" {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(u) != u || strings.ContainsFunc(u, func(r rune) bool { return r < 0x20 || r == 0x7f }) {
|
||||
return false
|
||||
}
|
||||
if httpURL(u) {
|
||||
return true
|
||||
}
|
||||
if schemePrefix.MatchString(u) || u[0] == '#' || u[0] == '\\' {
|
||||
return false
|
||||
}
|
||||
return !strings.HasPrefix(u, "//") && !strings.HasPrefix(u, `/\`)
|
||||
}
|
||||
|
||||
// httpURL reports whether u starts with http:// or https://
|
||||
// (case-insensitive).
|
||||
func httpURL(u string) bool {
|
||||
lower := strings.ToLower(u)
|
||||
return strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://")
|
||||
}
|
||||
|
||||
@@ -55,3 +55,30 @@ func TestURLTo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllowedItemURL pins the save rule of a url item's address (CR-01).
|
||||
func TestAllowedItemURL(t *testing.T) {
|
||||
for u, want := range map[string]bool{
|
||||
"": true,
|
||||
"/": true,
|
||||
"/about": true,
|
||||
"about/team?x=1": true,
|
||||
"https://example.test/a": true,
|
||||
"HTTP://example.test/a": true,
|
||||
"javascript://%0aalert(1)%0a//x": false,
|
||||
"javascript:alert(1)": false,
|
||||
"data:text/html,x": false,
|
||||
"mailto:a@example.test": false,
|
||||
"//evil.test/x": false,
|
||||
`/\evil.test/x`: false,
|
||||
`\\evil.test`: false,
|
||||
"#top": false,
|
||||
" /about": false,
|
||||
"/about\n": false,
|
||||
"java\tscript:alert(1)": false,
|
||||
} {
|
||||
if got := classes.AllowedItemURL(u); got != want {
|
||||
t.Errorf("AllowedItemURL(%q) = %v, want %v", u, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user