fix: a URL rule's address is capped at 2048 bytes (WR-23)

Rule.Problems had no length check, so a long address reached the
(url, match) UNIQUE btree index and failed with a 500. It now refuses an
address over models.MaxRuleURL with rules.url_invalid, like redirect
sources.
This commit is contained in:
2026-10-27 15:41:55 +01:00
parent 7677d8d8af
commit 4ca26e9a89
2 changed files with 20 additions and 4 deletions

View File

@@ -268,6 +268,16 @@ func TestManagementAPI(t *testing.T) {
t.Fatalf("set_url_seo locale pl = %v", got)
}
// A rule address is capped like a redirect source, so a long one is a
// field refusal, not a database error from the unique index (WR-23).
long := "/" + strings.Repeat("x", 3000)
if rec := env.mcp(t, http.MethodPost, "/_seo/api/v1/rules", agent, map[string]any{"url": long, "title": "Long"}); rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), `"url"`) {
t.Fatalf("create_rule with a 3001-byte url = %d %s, want 422 on url", rec.Code, rec.Body.String())
}
if rec := env.mcp(t, http.MethodPut, "/_seo/api/v1/seo", agent, map[string]any{"url": long, "values": map[string]any{"title": "Long"}}); rec.Code != http.StatusUnprocessableEntity {
t.Fatalf("set_url_seo on a 3001-byte url = %d %s, want 422", rec.Code, rec.Body.String())
}
// URL rules.
rule := env.mcpExpect(t, http.StatusCreated, http.MethodPost, "/_seo/api/v1/rules", agent, map[string]any{"url": "/blog?tag=*", "match": "pattern", "robots": "noindex"})
data, _ := rule["data"].(map[string]any)

View File

@@ -96,14 +96,20 @@ func NormalizeMatch(m string) string {
return MatchExact
}
// Problems refuses an address that does not start with "/" or carries * in
// an exact rule, and an address and match another rule already has (db is
// the save transaction; nil skips the uniqueness check).
// MaxRuleURL caps a URL rule's address in bytes, like MaxRedirectURL: the
// (url, match) UNIQUE btree index refuses rows over about 2.7 KB with a
// database error (WR-23).
const MaxRuleURL = 2048
// Problems refuses an address that does not start with "/", is longer than
// MaxRuleURL or carries * in an exact rule, and an address and match
// another rule already has (db is the save transaction; nil skips the
// uniqueness check).
func (r Rule) Problems(db *gorm.DB) map[string][]Problem {
out := map[string][]Problem{}
url := strings.TrimSpace(r.URL)
match := NormalizeMatch(r.Match)
if !strings.HasPrefix(url, "/") || strings.HasPrefix(url, "//") || strings.ContainsAny(url, " \t\r\n") || (match == MatchExact && strings.Contains(url, "*")) {
if len(url) > MaxRuleURL || !strings.HasPrefix(url, "/") || strings.HasPrefix(url, "//") || strings.ContainsAny(url, " \t\r\n") || (match == MatchExact && strings.Contains(url, "*")) {
out["url"] = []Problem{{Key: errRuleURLInvalid}}
return out
}