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: | |
| 77 | 77 | - "conditions": a whitespace-separated list of "key=value" | |
| 78 | 78 | - "Sign" is a special condition | |
| 79 | 79 | */ | |
| 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 | + | ||
| 80 | 106 | func parseRedirectText(text string) ([]*RedirectRule, error) { | |
| 81 | 107 | rules := []*RedirectRule{} | |
| 82 | 108 | origLines := strings.Split(text, "\n") |
| ... | ... | @@ -131,6 +157,11 @@ func parseRedirectText(text string) ([]*RedirectRule, error) { | |
| 131 | 157 | conditions = parsePairs(lastParts[1:]) | |
| 132 | 158 | } | |
| 133 | 159 | ||
| 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 | + | ||
| 134 | 165 | rules = append(rules, &RedirectRule{ | |
| 135 | 166 | To: to, | |
| 136 | 167 | From: from, |
+54,
-0
| ... | ... | @@ -92,6 +92,55 @@ func TestParseRedirectText(t *testing.T) { | |
| 92 | 92 | }, | |
| 93 | 93 | } | |
| 94 | 94 | ||
| 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 | + | ||
| 95 | 144 | fixtures := []RedirectFixture{ | |
| 96 | 145 | spa, | |
| 97 | 146 | rss, |
| ... | ... | @@ -99,6 +148,11 @@ func TestParseRedirectText(t *testing.T) { | |
| 99 | 148 | noStatus, | |
| 100 | 149 | absoluteUriNoProto, | |
| 101 | 150 | absoluteUriWithProto, | |
| 151 | + | selfReferentialExact, | |
| 152 | + | selfReferentialWildcard, | |
| 153 | + | selfReferentialWithVariables, | |
| 154 | + | externalUrlNotSelfRef, | |
| 155 | + | validPathRedirect, | |
| 102 | 156 | } | |
| 103 | 157 | ||
| 104 | 158 | for _, fixture := range fixtures { |