From 4ca26e9a897ca434da28e3df649d54f6b6cf71a1 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 27 Oct 2026 15:41:55 +0100 Subject: [PATCH] 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. --- mcp_test.go | 10 ++++++++++ models/rule.go | 14 ++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/mcp_test.go b/mcp_test.go index 55aac30..1df70b1 100644 --- a/mcp_test.go +++ b/mcp_test.go @@ -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) diff --git a/models/rule.go b/models/rule.go index 9ab7c04..d20a625 100644 --- a/models/rule.go +++ b/models/rule.go @@ -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 }