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:
2026-10-12 21:28:28 +02:00
parent 4e55939a1f
commit c42bc265c8
12 changed files with 257 additions and 11 deletions

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"os"
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
"git.golem15.com/golem15/sm-sitemap-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bonfire"
@@ -22,7 +23,8 @@ import (
// its url items; with --append the URLs are added after every existing item.
// The definition is always the one /sitemap.xml serves (theme default);
// --theme is accepted for PHP compatibility and a theme other than default
// is reported.
// is reported. An entry whose url is not a path or an http(s) URL
// (classes.AllowedItemURL) is refused, as the admin form refuses it.
func Import(app *backpack.App) bonfire.Command {
return bonfire.Command{
Name: "sitemap:import",
@@ -67,6 +69,9 @@ func ImportFile(ctx context.Context, db *gorm.DB, file, theme string, appendMode
if fields["url"] == nil || err != nil || phpEmpty(fields["url"]) {
return fmt.Errorf("Entry %s is missing a 'url' field", labels[i])
}
if !classes.AllowedItemURL(models.Text(url)) {
return fmt.Errorf("Entry %s has a url that is not a path or an http(s) URL", labels[i])
}
item := models.DefinitionItem{Type: models.ItemTypeURL, URL: url}
item.Changefreq = textOr(fields["changefreq"], "monthly")
item.Priority = textOr(fields["priority"], "0.5")

View File

@@ -12,6 +12,7 @@ import (
"sort"
"strings"
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
"git.golem15.com/golem15/sm-sitemap-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bonfire"
@@ -65,9 +66,12 @@ type WinterReport struct {
// their URL, --map old=new renames applied first),
// an item of a registry type that takes references is dropped when its
// reference is not among the type's current references, an item of an
// unknown type is dropped, and every dropped or remapped item is printed. A
// definition that already has items is refused without --force. Settings
// keys missing from the export take the PHP defaults.
// unknown type is dropped, a url item whose URL is not a path or an http(s)
// URL (classes.AllowedItemURL) is dropped, a URL pattern that is not a path
// (signpost.ValidItemPattern) is cleared, and every dropped, remapped or
// cleared item is printed. A definition that already has items is refused
// without --force. Settings keys missing from the export take the PHP
// defaults.
func ImportWinter(app *backpack.App) bonfire.Command {
return bonfire.Command{
Name: "sitemap:import-winter",
@@ -328,7 +332,16 @@ func mapItems(ctx context.Context, reg *signpost.Registry, data json.RawMessage,
if page, ok := fields["cmsPage"].(string); ok && page != "" {
report.Lines = append(report.Lines, fmt.Sprintf("%s: cmsPage '%s' dropped (CMS pages are not supported)", label, page))
}
if p := models.Text(item.URLPattern); p != "" && !signpost.ValidItemPattern(p) {
report.Lines = append(report.Lines, fmt.Sprintf("%s: urlPattern '%s' cleared (not a path starting with a single /)", label, p))
item.URLPattern = nil
}
if url, ok := pageURLs[models.Text(item.Reference)]; ok && droppedTypes[item.Type] {
if !classes.AllowedItemURL(url) {
report.Lines = append(report.Lines, fmt.Sprintf("%s: dropped (--page-url '%s' is not a path or an http(s) URL)", label, url))
report.Dropped++
continue
}
report.Lines = append(report.Lines, fmt.Sprintf("%s: copied as type 'url' with url '%s'", label, url))
report.Remapped++
item.Type = models.ItemTypeURL
@@ -349,6 +362,11 @@ func mapItems(ctx context.Context, reg *signpost.Registry, data json.RawMessage,
}
}
if item.Type == models.ItemTypeURL {
if !classes.AllowedItemURL(models.Text(item.URL)) {
report.Lines = append(report.Lines, fmt.Sprintf("%s: dropped (the url is not a path or an http(s) URL)", label))
report.Dropped++
continue
}
out = append(out, item)
report.Kept++
continue