Skip to content

Commit d2055fd

Browse files
committed
fix(translator): strip scheme from CSRF additional_origins before xDS translation
Envoy's CSRF filter calls hostAndPort() on the Origin header before matching against additional_origins, which strips the scheme (e.g., "https://") and compares only host:port. However, the xDS translation was passing origin values with schemes intact, causing exact, prefix, and regex matches to always fail (suffix matches worked because they typically don't include a scheme). This fix strips the scheme from StringMatch values in buildXdsCSRFOrigins before passing them to Envoy, so users can write natural full URLs (e.g., "https://www.example.com") and the translation handles the conversion. Fixes E2E test failures: CSRFFromSecurityPolicy/should_allow_POST_with_matching_exact_Origin, CSRFFromSecurityPolicy/should_allow_POST_with_matching_prefix_Origin, CSRFFromSecurityPolicy/should_allow_POST_with_matching_regex_Origin. Signed-off-by: asalvador <asalvador@newrelic.com>
1 parent e098b7f commit d2055fd

3 files changed

Lines changed: 63 additions & 4 deletions

File tree

internal/xds/translator/csrf.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package translator
88
import (
99
"errors"
1010
"fmt"
11+
"net/url"
1112

1213
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
1314
routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
@@ -155,14 +156,68 @@ func (*csrf) patchResources(*types.ResourceVersionTable, []*ir.HTTPRoute) error
155156
}
156157

157158
// buildXdsCSRFOrigins converts IR StringMatches to Envoy StringMatchers for CSRF origins.
159+
// Envoy's CSRF filter strips the scheme from the Origin header before matching
160+
// (it calls hostAndPort() which extracts only host:port from the URL).
161+
// We must strip schemes from matcher values so they match what Envoy actually compares.
158162
func buildXdsCSRFOrigins(csrf *ir.CSRF) []*matcherv3.StringMatcher {
159163
if csrf == nil {
160164
return nil
161165
}
162166

163167
var origins []*matcherv3.StringMatcher
164168
for _, origin := range csrf.AdditionalOrigins {
165-
origins = append(origins, buildXdsStringMatcher(origin))
169+
stripped := stripSchemeFromStringMatch(origin)
170+
origins = append(origins, buildXdsStringMatcher(stripped))
166171
}
167172
return origins
168173
}
174+
175+
// stripSchemeFromStringMatch removes the scheme (e.g., "https://") from a
176+
// StringMatch value. This is needed because Envoy's CSRF filter compares
177+
// against host:port only, not the full origin URL.
178+
func stripSchemeFromStringMatch(m *ir.StringMatch) *ir.StringMatch {
179+
out := &ir.StringMatch{
180+
Name: m.Name,
181+
Distinct: m.Distinct,
182+
}
183+
switch {
184+
case m.Exact != nil:
185+
v := stripScheme(*m.Exact)
186+
out.Exact = &v
187+
case m.Prefix != nil:
188+
v := stripScheme(*m.Prefix)
189+
out.Prefix = &v
190+
case m.Suffix != nil:
191+
// Suffix values typically don't include a scheme, but strip just in case.
192+
v := stripScheme(*m.Suffix)
193+
out.Suffix = &v
194+
case m.SafeRegex != nil:
195+
v := stripSchemeFromRegex(*m.SafeRegex)
196+
out.SafeRegex = &v
197+
}
198+
return out
199+
}
200+
201+
// stripScheme removes the scheme prefix (e.g., "https://") from a URL string.
202+
func stripScheme(origin string) string {
203+
if u, err := url.Parse(origin); err == nil && u.Scheme != "" && u.Host != "" {
204+
return u.Host
205+
}
206+
return origin
207+
}
208+
209+
// stripSchemeFromRegex removes a leading scheme pattern from a regex.
210+
// Common patterns: "https://.*" -> ".*", "https?://foo" -> "foo"
211+
func stripSchemeFromRegex(regex string) string {
212+
prefixes := []string{
213+
`https?://`,
214+
`https://`,
215+
`http://`,
216+
}
217+
for _, p := range prefixes {
218+
if len(regex) > len(p) && regex[:len(p)] == p {
219+
return regex[len(p):]
220+
}
221+
}
222+
return regex
223+
}

internal/xds/translator/testdata/out/xds-ir/csrf.routes.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
envoy.filters.http.csrf:
1717
'@type': type.googleapis.com/envoy.extensions.filters.http.csrf.v3.CsrfPolicy
1818
additionalOrigins:
19-
- exact: https://www.example.com
20-
- prefix: https://app.example
19+
- exact: www.example.com
20+
- prefix: app.example
2121
- suffix: .trusted.com
2222
- safeRegex:
23-
regex: https://.*\.partner\.com
23+
regex: .*\.partner\.com
2424
filterEnabled:
2525
defaultValue:
2626
numerator: 100

site/content/en/latest/tasks/security/csrf.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ When CSRF protection is enabled, the Envoy CSRF filter validates that the `Origi
1919
(POST, PUT, DELETE, PATCH) matches the destination or one of the configured additional origins.
2020
Non-mutating requests (GET, HEAD, OPTIONS) are not affected.
2121

22+
Note: Envoy's CSRF filter compares against the host and port of the origin, not the full URL.
23+
Envoy Gateway automatically strips the scheme (e.g., `https://`) from configured origins before
24+
passing them to Envoy, so you can write origins as full URLs for readability.
25+
2226
The below example defines a SecurityPolicy that enables CSRF protection and allows additional origins
2327
matching `https://www.example.com` exactly and any subdomain of `trusted.com` via regex.
2428

0 commit comments

Comments
 (0)