From 8f94b079867890743ecc2adea887e72335032f7c Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 17 Sep 2026 13:31:58 +0200 Subject: [PATCH] fix(02-03): scrub PHP-escaped JSON secret values PHP json_encode writes \/ so captured redirect URLs never matched the fixture body, leaving OAuth codes in client sessions. Co-authored-by: Cursor --- tide/capture_test.go | 25 +++++++++++++++++++++++++ tide/variables.go | 18 ++++++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/tide/capture_test.go b/tide/capture_test.go index 29ca171..d1fc16f 100644 --- a/tide/capture_test.go +++ b/tide/capture_test.go @@ -55,6 +55,31 @@ func TestCaptureAndPlaceholderResolution(t *testing.T) { t.Fatalf("missing placeholder: %v", err) } + escaped := Step{ + ID: "consent", + Response: Response{ + Body: Body(`{"data":{"redirect_to":"http:\/\/127.0.0.1:8424\/oauth\/callback?code=oauthCode99"}}`), + }, + Capture: []CaptureRule{{ + From: "response.json", + Path: "$.data.redirect_to", + As: "oauth:redirect", + Category: "oauth_code", + }}, + } + if err := CaptureStep(store, &escaped); err != nil { + t.Fatal(err) + } + if err := ScrubStep(store, &escaped); err != nil { + t.Fatal(err) + } + if strings.Contains(string(escaped.Response.Body), "oauthCode99") { + t.Fatalf("php-escaped redirect still has code: %s", escaped.Response.Body) + } + if !strings.Contains(string(escaped.Response.Body), "{{oauth:redirect}}") { + t.Fatalf("php-escaped redirect not placeholder: %s", escaped.Response.Body) + } + step2 := Step{ ID: "pkce", Request: Request{ diff --git a/tide/variables.go b/tide/variables.go index 1084f39..57c4bda 100644 --- a/tide/variables.go +++ b/tide/variables.go @@ -476,15 +476,25 @@ func replaceAll(s string, pairs [][2]string) string { if p[0] == "" { continue } - if len(p[0]) >= 8 { - s = strings.ReplaceAll(s, p[0], p[1]) - continue + olds := []string{p[0]} + if esc := phpJSONEscape(p[0]); esc != p[0] { + olds = append(olds, esc) + } + for _, old := range olds { + if len(old) >= 8 { + s = strings.ReplaceAll(s, old, p[1]) + continue + } + s = replaceIsolated(s, old, p[1]) } - s = replaceIsolated(s, p[0], p[1]) } return s } +func phpJSONEscape(s string) string { + return strings.ReplaceAll(s, "/", `\/`) +} + func replaceIsolated(s, old, neu string) string { if old == "" || s == "" { return s