Skip to content

Commit 30b56e4

Browse files
authored
Fix literal plus preservation in challenge redirects (#92)
1 parent 634c476 commit 30b56e4

2 files changed

Lines changed: 41 additions & 16 deletions

File tree

main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,10 @@ func normalizeDestination(destination string) string {
705705
return "/"
706706
}
707707

708-
unescaped, err := url.QueryUnescape(destination)
708+
// The form parser has already applied application/x-www-form-urlencoded
709+
// decoding. Use path semantics for destinations that are still escaped so
710+
// literal plus signs are not decoded a second time as spaces.
711+
unescaped, err := url.PathUnescape(destination)
709712
if err == nil && unescaped != destination {
710713
if sanitized := sanitizeDestination(unescaped); sanitized != "/" || unescaped == "/" {
711714
return sanitized

main_test.go

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,12 +1211,13 @@ func TestStateBookkeepingErrorBranches(t *testing.T) {
12111211

12121212
func TestVerifyChallengePage(t *testing.T) {
12131213
tests := []struct {
1214-
name string
1215-
provider string
1216-
formValues map[string]string
1217-
mockResponse string
1218-
expectedStatus int
1219-
shouldSetCache bool
1214+
name string
1215+
provider string
1216+
formValues map[string]string
1217+
mockResponse string
1218+
expectedStatus int
1219+
shouldSetCache bool
1220+
expectedLocation string
12201221
}{
12211222
{
12221223
name: "Missing captcha response",
@@ -1232,19 +1233,33 @@ func TestVerifyChallengePage(t *testing.T) {
12321233
"cf-turnstile-response": "valid-token",
12331234
"destination": "%2Fhome",
12341235
},
1235-
mockResponse: `{"success":true}`,
1236-
expectedStatus: http.StatusFound,
1237-
shouldSetCache: true,
1236+
mockResponse: `{"success":true}`,
1237+
expectedStatus: http.StatusFound,
1238+
shouldSetCache: true,
1239+
expectedLocation: "/home",
1240+
},
1241+
{
1242+
name: "Successful verification preserves literal plus",
1243+
provider: "turnstile",
1244+
formValues: map[string]string{
1245+
"cf-turnstile-response": "valid-token",
1246+
"destination": "/Chrome%20+%20MariaDB%20",
1247+
},
1248+
mockResponse: `{"success":true}`,
1249+
expectedStatus: http.StatusFound,
1250+
shouldSetCache: true,
1251+
expectedLocation: "/Chrome%20+%20MariaDB%20",
12381252
},
12391253
{
12401254
name: "Successful verification without destination",
12411255
provider: "recaptcha",
12421256
formValues: map[string]string{
12431257
"g-recaptcha-response": "valid-token",
12441258
},
1245-
mockResponse: `{"success":true}`,
1246-
expectedStatus: http.StatusFound,
1247-
shouldSetCache: true,
1259+
mockResponse: `{"success":true}`,
1260+
expectedStatus: http.StatusFound,
1261+
shouldSetCache: true,
1262+
expectedLocation: "/",
12481263
},
12491264
{
12501265
name: "Failed verification",
@@ -1263,9 +1278,10 @@ func TestVerifyChallengePage(t *testing.T) {
12631278
"cf-turnstile-response": "valid-token",
12641279
"destination": "%ZZ",
12651280
},
1266-
mockResponse: `{"success":true}`,
1267-
expectedStatus: http.StatusFound,
1268-
shouldSetCache: true,
1281+
mockResponse: `{"success":true}`,
1282+
expectedStatus: http.StatusFound,
1283+
shouldSetCache: true,
1284+
expectedLocation: "/",
12691285
},
12701286
}
12711287

@@ -1310,6 +1326,10 @@ func TestVerifyChallengePage(t *testing.T) {
13101326
if found != tt.shouldSetCache {
13111327
t.Errorf("Expected cache set=%v, got=%v", tt.shouldSetCache, found)
13121328
}
1329+
1330+
if tt.expectedLocation != "" && rr.Header().Get("Location") != tt.expectedLocation {
1331+
t.Errorf("Expected Location %q, got %q", tt.expectedLocation, rr.Header().Get("Location"))
1332+
}
13131333
})
13141334
}
13151335
}
@@ -2106,6 +2126,8 @@ func TestNormalizeDestination(t *testing.T) {
21062126
{name: "empty", destination: "", want: "/"},
21072127
{name: "decoded local path", destination: "/home?foo=bar", want: "/home?foo=bar"},
21082128
{name: "encoded local path", destination: "%2Fhome%3Ffoo%3Dbar", want: "/home?foo=bar"},
2129+
{name: "decoded path with literal plus", destination: "/Chrome%20+%20MariaDB%20", want: "/Chrome%20+%20MariaDB%20"},
2130+
{name: "encoded path with literal plus", destination: "%2FChrome%2520%2B%2520MariaDB%2520", want: "/Chrome%20+%20MariaDB%20"},
21092131
{name: "absolute url", destination: "https://evil.com/phish", want: "/"},
21102132
{name: "protocol relative url", destination: "//evil.com/phish", want: "/"},
21112133
{name: "relative path", destination: "home", want: "/"},

0 commit comments

Comments
 (0)