Commit ccc233b

Eric Bower  ·  2026-02-16 20:15:23 -0500 EST
parent a4ff178
chore(pgs): tests to prevent infinite redirects
2 files changed,  +85, -0
+31, -0
......@@ -77,6 +77,32 @@ The parts are:
7777 - "conditions": a whitespace-separated list of "key=value"
7878 - "Sign" is a special condition
7979 */
80+// isSelfReferentialRedirect checks if a redirect rule would redirect to itself.
81+// This includes exact matches and wildcard patterns that would match the same path.
82+func isSelfReferentialRedirect(from, to string) bool {
83+ // External URLs are never self-referential
84+ if isUrl(to) {
85+ return false
86+ }
87+
88+ // Exact match: /page redirects to /page
89+ if from == to {
90+ return true
91+ }
92+
93+ // Wildcard match: /* redirects to /*
94+ if from == to && strings.Contains(from, "*") {
95+ return true
96+ }
97+
98+ // Pattern with variable: /:path redirects to /:path
99+ if from == to && strings.Contains(from, ":") {
100+ return true
101+ }
102+
103+ return false
104+}
105+
80106 func parseRedirectText(text string) ([]*RedirectRule, error) {
81107 rules := []*RedirectRule{}
82108 origLines := strings.Split(text, "\n")
......@@ -131,6 +157,11 @@ func parseRedirectText(text string) ([]*RedirectRule, error) {
131157 conditions = parsePairs(lastParts[1:])
132158 }
133159
160+ // Validate that the redirect is not self-referential
161+ if isSelfReferentialRedirect(from, to) {
162+ return rules, fmt.Errorf("self-referential redirect: '%s' cannot redirect to itself", from)
163+ }
164+
134165 rules = append(rules, &RedirectRule{
135166 To: to,
136167 From: from,
+54, -0
......@@ -92,6 +92,55 @@ func TestParseRedirectText(t *testing.T) {
9292 },
9393 }
9494
95+ selfReferentialExact := RedirectFixture{
96+ name: "self-referential-exact",
97+ input: "/page /page 301",
98+ expect: []*RedirectRule{},
99+ shouldError: true,
100+ }
101+
102+ selfReferentialWildcard := RedirectFixture{
103+ name: "self-referential-wildcard",
104+ input: "/* /* 301",
105+ expect: []*RedirectRule{},
106+ shouldError: true,
107+ }
108+
109+ selfReferentialWithVariables := RedirectFixture{
110+ name: "self-referential-with-variables",
111+ input: "/:path /:path 301",
112+ expect: []*RedirectRule{},
113+ shouldError: true,
114+ }
115+
116+ externalUrlNotSelfRef := RedirectFixture{
117+ name: "external-url-not-self-referential",
118+ input: "/* https://example.com 301",
119+ expect: []*RedirectRule{
120+ {
121+ From: "/*",
122+ To: "https://example.com",
123+ Status: 301,
124+ Query: empty,
125+ Conditions: empty,
126+ },
127+ },
128+ }
129+
130+ validPathRedirect := RedirectFixture{
131+ name: "valid-path-redirect",
132+ input: "/old-path /new-path 301",
133+ expect: []*RedirectRule{
134+ {
135+ From: "/old-path",
136+ To: "/new-path",
137+ Status: 301,
138+ Query: empty,
139+ Conditions: empty,
140+ },
141+ },
142+ }
143+
95144 fixtures := []RedirectFixture{
96145 spa,
97146 rss,
......@@ -99,6 +148,11 @@ func TestParseRedirectText(t *testing.T) {
99148 noStatus,
100149 absoluteUriNoProto,
101150 absoluteUriWithProto,
151+ selfReferentialExact,
152+ selfReferentialWildcard,
153+ selfReferentialWithVariables,
154+ externalUrlNotSelfRef,
155+ validPathRedirect,
102156 }
103157
104158 for _, fixture := range fixtures {