Skip to content

Commit fc8c1ea

Browse files
committed
api: reuse the CORS Origin type for csrf additionalOrigins
additionalOrigins described a list of trusted origins with a generic StringMatch, so the same concept was expressed two different ways inside one SecurityPolicy: cors.allowOrigins took "https://*.example.com" while csrf.additionalOrigins took {type: Suffix, value: .example.com}. Reuse Origin so both read the same, and drop the CEL rule that had to reject schemes and paths, since the Origin pattern already constrains the shape. Envoy accepts a StringMatcher for cors allow_origin_string_match and for csrf additional_origins alike, so the narrower syntax is an Envoy Gateway choice rather than a filter limitation, and it is the choice CORS already made. It also drops the two matchers that widen trust by accident: a Prefix of "app.example" matches the attacker-controlled app.example.attacker.com, and a Suffix that omits the leading dot matches eviltrusted.com. A wildcard label goes through wildcard2regex, which escapes the separators and anchors the result, so neither hole is reachable. Envoy compares the host and port of the origin, so buildCSRF strips the scheme when lowering to the IR matcher: an origin with a wildcard host becomes an anchored regex, everything else an exact match on host[:port]. Signed-off-by: Huabing (Robin) Zhao <zhaohuabing@gmail.com>
1 parent 9e6dd18 commit fc8c1ea

17 files changed

Lines changed: 274 additions & 260 deletions

File tree

api/v1alpha1/csrf_types.go

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,6 @@ import (
1313
// The CSRF filter checks that the Origin header in HTTP requests matches the destination,
1414
// preventing cross-origin mutating requests (POST, PUT, DELETE, PATCH) from being processed.
1515
// GET and HEAD requests are always allowed.
16-
//
17-
// Note: Envoy's CSRF filter compares against the host and port of the origin only
18-
// (the scheme is stripped before matching). Additional origins must be specified as
19-
// host or host:port values, not full URLs. For example, use "www.example.com"
20-
// instead of "https://www.example.com".
21-
//
22-
// +kubebuilder:validation:XValidation:message="additionalOrigins must be host or host:port values without a scheme or path, for example www.example.com instead of https://www.example.com",rule="!has(self.additionalOrigins) || self.additionalOrigins.all(o, !o.value.contains('/'))"
2316
type CSRF struct {
2417
// ShadowFraction represents the fraction of requests for which the CSRF policy is
2518
// evaluated in shadow (dry-run) mode. For these requests, the filter records whether
@@ -35,12 +28,17 @@ type CSRF struct {
3528
// +optional
3629
ShadowFraction *gwapiv1.Fraction `json:"shadowFraction,omitempty"`
3730

38-
// AdditionalOrigins specifies additional origins that are allowed to make requests,
39-
// beyond the destination origin. These are checked against the Origin header (host:port only,
40-
// not the full URL) and if matched, the request is allowed.
41-
// Each origin supports Exact, Prefix, Suffix, and RegularExpression matching.
31+
// AdditionalOrigins specifies additional origins that are allowed to make mutating
32+
// requests, beyond the destination origin. A request whose Origin header matches one
33+
// of them is allowed. The value "*" allows any origin, which effectively disables
34+
// origin validation.
35+
//
36+
// Note: Envoy's CSRF filter compares the host and port of the origin only, so the
37+
// scheme is ignored: "https://www.example.com" and "http://www.example.com" are
38+
// equivalent here, and both allow the request regardless of the scheme the client
39+
// used.
4240
//
4341
// +optional
4442
// +kubebuilder:validation:MaxItems=16
45-
AdditionalOrigins []StringMatch `json:"additionalOrigins,omitempty"`
43+
AdditionalOrigins []Origin `json:"additionalOrigins,omitempty"`
4644
}

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 2 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -700,34 +700,36 @@ spec:
700700
properties:
701701
additionalOrigins:
702702
description: |-
703-
AdditionalOrigins specifies additional origins that are allowed to make requests,
704-
beyond the destination origin. These are checked against the Origin header (host:port only,
705-
not the full URL) and if matched, the request is allowed.
706-
Each origin supports Exact, Prefix, Suffix, and RegularExpression matching.
703+
AdditionalOrigins specifies additional origins that are allowed to make mutating
704+
requests, beyond the destination origin. A request whose Origin header matches one
705+
of them is allowed. The value "*" allows any origin, which effectively disables
706+
origin validation.
707+
708+
Note: Envoy's CSRF filter compares the host and port of the origin only, so the
709+
scheme is ignored: "https://www.example.com" and "http://www.example.com" are
710+
equivalent here, and both allow the request regardless of the scheme the client
711+
used.
707712
items:
708713
description: |-
709-
StringMatch defines how to match any strings.
710-
This is a general purpose match condition that can be used by other EG APIs
711-
that need to match against a string.
712-
properties:
713-
type:
714-
default: Exact
715-
description: Type specifies how to match against a string.
716-
enum:
717-
- Exact
718-
- Prefix
719-
- Suffix
720-
- RegularExpression
721-
type: string
722-
value:
723-
description: Value specifies the string value that the match
724-
must have.
725-
maxLength: 1024
726-
minLength: 1
727-
type: string
728-
required:
729-
- value
730-
type: object
714+
Origin is defined by the scheme (protocol), hostname (domain), and port of
715+
the URL used to access it. The hostname can be "precise" which is just the
716+
domain name or "wildcard" which is a domain name prefixed with a single
717+
wildcard label such as "*.example.com".
718+
In addition to that a single wildcard (with or without scheme) can be
719+
configured to match any origin.
720+
721+
For example, the following are valid origins:
722+
- https://foo.example.com
723+
- https://*.example.com
724+
- http://foo.example.com:8080
725+
- http://*.example.com:8080
726+
- https://*
727+
- moz-extension://example.com
728+
- foo://*.example.com:8080
729+
maxLength: 253
730+
minLength: 1
731+
pattern: ^(\*|[A-Za-z][A-Za-z0-9+.-]*:\/\/(\*|(\*\.)?(([\w-]+\.?)+)?[\w-]+)(:\d{1,5})?)$
732+
type: string
731733
maxItems: 16
732734
type: array
733735
shadowFraction:
@@ -759,11 +761,6 @@ spec:
759761
- message: numerator must be less than or equal to denominator
760762
rule: self.numerator <= self.denominator
761763
type: object
762-
x-kubernetes-validations:
763-
- message: additionalOrigins must be host or host:port values without
764-
a scheme or path, for example www.example.com instead of https://www.example.com
765-
rule: '!has(self.additionalOrigins) || self.additionalOrigins.all(o,
766-
!o.value.contains(''/''))'
767764
extAuth:
768765
description: ExtAuth defines the configuration for External Authorization.
769766
properties:

charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -699,34 +699,36 @@ spec:
699699
properties:
700700
additionalOrigins:
701701
description: |-
702-
AdditionalOrigins specifies additional origins that are allowed to make requests,
703-
beyond the destination origin. These are checked against the Origin header (host:port only,
704-
not the full URL) and if matched, the request is allowed.
705-
Each origin supports Exact, Prefix, Suffix, and RegularExpression matching.
702+
AdditionalOrigins specifies additional origins that are allowed to make mutating
703+
requests, beyond the destination origin. A request whose Origin header matches one
704+
of them is allowed. The value "*" allows any origin, which effectively disables
705+
origin validation.
706+
707+
Note: Envoy's CSRF filter compares the host and port of the origin only, so the
708+
scheme is ignored: "https://www.example.com" and "http://www.example.com" are
709+
equivalent here, and both allow the request regardless of the scheme the client
710+
used.
706711
items:
707712
description: |-
708-
StringMatch defines how to match any strings.
709-
This is a general purpose match condition that can be used by other EG APIs
710-
that need to match against a string.
711-
properties:
712-
type:
713-
default: Exact
714-
description: Type specifies how to match against a string.
715-
enum:
716-
- Exact
717-
- Prefix
718-
- Suffix
719-
- RegularExpression
720-
type: string
721-
value:
722-
description: Value specifies the string value that the match
723-
must have.
724-
maxLength: 1024
725-
minLength: 1
726-
type: string
727-
required:
728-
- value
729-
type: object
713+
Origin is defined by the scheme (protocol), hostname (domain), and port of
714+
the URL used to access it. The hostname can be "precise" which is just the
715+
domain name or "wildcard" which is a domain name prefixed with a single
716+
wildcard label such as "*.example.com".
717+
In addition to that a single wildcard (with or without scheme) can be
718+
configured to match any origin.
719+
720+
For example, the following are valid origins:
721+
- https://foo.example.com
722+
- https://*.example.com
723+
- http://foo.example.com:8080
724+
- http://*.example.com:8080
725+
- https://*
726+
- moz-extension://example.com
727+
- foo://*.example.com:8080
728+
maxLength: 253
729+
minLength: 1
730+
pattern: ^(\*|[A-Za-z][A-Za-z0-9+.-]*:\/\/(\*|(\*\.)?(([\w-]+\.?)+)?[\w-]+)(:\d{1,5})?)$
731+
type: string
730732
maxItems: 16
731733
type: array
732734
shadowFraction:
@@ -758,11 +760,6 @@ spec:
758760
- message: numerator must be less than or equal to denominator
759761
rule: self.numerator <= self.denominator
760762
type: object
761-
x-kubernetes-validations:
762-
- message: additionalOrigins must be host or host:port values without
763-
a scheme or path, for example www.example.com instead of https://www.example.com
764-
rule: '!has(self.additionalOrigins) || self.additionalOrigins.all(o,
765-
!o.value.contains(''/''))'
766763
extAuth:
767764
description: ExtAuth defines the configuration for External Authorization.
768765
properties:

internal/gatewayapi/securitypolicy.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,16 +1753,39 @@ func (t *Translator) buildCORS(cors *egv1a1.CORS) *ir.CORS {
17531753
}
17541754

17551755
func (t *Translator) buildCSRF(csrf *egv1a1.CSRF) *ir.CSRF {
1756-
additionalOrigins := make([]*ir.StringMatch, 0, len(csrf.AdditionalOrigins))
1756+
var additionalOrigins []*ir.StringMatch
1757+
17571758
for _, origin := range csrf.AdditionalOrigins {
1758-
additionalOrigins = append(additionalOrigins, irStringMatch("csrf", origin))
1759+
// Envoy's CSRF filter matches the host and port of the Origin header against the
1760+
// target host, so the scheme is dropped before the matcher is built.
1761+
hostAndPort := originHostAndPort(string(origin))
1762+
if containsWildcard(hostAndPort) {
1763+
regexStr := wildcard2regex(hostAndPort)
1764+
additionalOrigins = append(additionalOrigins, &ir.StringMatch{
1765+
SafeRegex: &regexStr,
1766+
})
1767+
} else {
1768+
additionalOrigins = append(additionalOrigins, &ir.StringMatch{
1769+
Exact: &hostAndPort,
1770+
})
1771+
}
17591772
}
1773+
17601774
return &ir.CSRF{
17611775
ShadowFraction: csrf.ShadowFraction,
17621776
AdditionalOrigins: additionalOrigins,
17631777
}
17641778
}
17651779

1780+
// originHostAndPort strips the scheme from an Origin, leaving its host and optional port.
1781+
// A bare "*" carries no scheme and is returned as is.
1782+
func originHostAndPort(origin string) string {
1783+
if _, hostAndPort, found := strings.Cut(origin, "://"); found {
1784+
return hostAndPort
1785+
}
1786+
return origin
1787+
}
1788+
17661789
func containsWildcard(s string) bool {
17671790
return strings.ContainsAny(s, "*")
17681791
}

internal/gatewayapi/testdata/securitypolicy-with-csrf.in.yaml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,11 @@ securityPolicies:
1313
shadowFraction:
1414
numerator: 20
1515
additionalOrigins:
16-
- type: Exact
17-
value: www.example.com
18-
- type: Prefix
19-
value: app.example
20-
- type: Suffix
21-
value: .trusted.com
22-
- type: RegularExpression
23-
value: ".*\\.partner\\.com$"
16+
- https://www.example.com
17+
- http://www.example.com:8080
18+
- https://*.trusted.com
19+
- https://*
20+
- "*"
2421
httpRoutes:
2522
- apiVersion: gateway.networking.k8s.io/v1
2623
kind: HTTPRoute

internal/gatewayapi/testdata/securitypolicy-with-csrf.out.yaml

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,11 @@ securityPolicies:
104104
spec:
105105
csrf:
106106
additionalOrigins:
107-
- type: Exact
108-
value: www.example.com
109-
- type: Prefix
110-
value: app.example
111-
- type: Suffix
112-
value: .trusted.com
113-
- type: RegularExpression
114-
value: .*\.partner\.com$
107+
- https://www.example.com
108+
- http://www.example.com:8080
109+
- https://*.trusted.com
110+
- https://*
111+
- '*'
115112
shadowFraction:
116113
numerator: 20
117114
targetRef:
@@ -215,16 +212,19 @@ xdsIR:
215212
additionalOrigins:
216213
- distinct: false
217214
exact: www.example.com
218-
name: csrf
215+
name: ""
216+
- distinct: false
217+
exact: www.example.com:8080
218+
name: ""
219219
- distinct: false
220-
name: csrf
221-
prefix: app.example
220+
name: ""
221+
safeRegex: .*\.trusted\.com
222222
- distinct: false
223-
name: csrf
224-
suffix: .trusted.com
223+
name: ""
224+
safeRegex: .*
225225
- distinct: false
226-
name: csrf
227-
safeRegex: .*\.partner\.com$
226+
name: ""
227+
safeRegex: .*
228228
shadowFraction:
229229
numerator: 20
230230
readyListener:

internal/xds/translator/testdata/in/xds-ir/csrf.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ http:
2323
csrf:
2424
additionalOrigins:
2525
- exact: www.example.com
26-
- prefix: app.example
27-
- suffix: .trusted.com
28-
- safeRegex: .*\.partner\.com$
26+
- exact: www.example.com:8080
27+
- safeRegex: .*\.trusted\.com
2928
- name: "second-route"
3029
hostname: "*"
3130
pathMatch:

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@
1717
'@type': type.googleapis.com/envoy.extensions.filters.http.csrf.v3.CsrfPolicy
1818
additionalOrigins:
1919
- exact: www.example.com
20-
- prefix: app.example
21-
- suffix: .trusted.com
20+
- exact: www.example.com:8080
2221
- safeRegex:
23-
regex: .*\.partner\.com$
22+
regex: .*\.trusted\.com
2423
filterEnabled:
2524
defaultValue:
2625
numerator: 100

site/content/en/latest/api/extension_types.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -846,18 +846,13 @@ The CSRF filter checks that the Origin header in HTTP requests matches the desti
846846
preventing cross-origin mutating requests (POST, PUT, DELETE, PATCH) from being processed.
847847
GET and HEAD requests are always allowed.
848848

849-
Note: Envoy's CSRF filter compares against the host and port of the origin only
850-
(the scheme is stripped before matching). Additional origins must be specified as
851-
host or host:port values, not full URLs. For example, use "www.example.com"
852-
instead of "https://www.example.com".
853-
854849
_Appears in:_
855850
- [SecurityPolicySpec](#securitypolicyspec)
856851

857852
| Field | Type | Required | Default | Description |
858853
| --- | --- | --- | --- | --- |
859854
| `shadowFraction` | _[Fraction](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#fraction)_ | false | | ShadowFraction represents the fraction of requests for which the CSRF policy is<br />evaluated in shadow (dry-run) mode. For these requests, the filter records whether<br />the request would have been allowed or rejected in the `csrf.request_valid` and<br />`csrf.request_invalid` stats, but always lets the request through. The remaining<br />requests are enforced, i.e. a mutating request with a missing or non-matching<br />Origin header is rejected with a 403.<br />Defaults to 0% (all requests are enforced) if not specified. Set it to 100% to<br />dry run the filter, watch the stats to find origins that would be rejected, then<br />lower it to roll enforcement out gradually. |
860-
| `additionalOrigins` | _[StringMatch](#stringmatch) array_ | false | | AdditionalOrigins specifies additional origins that are allowed to make requests,<br />beyond the destination origin. These are checked against the Origin header (host:port only,<br />not the full URL) and if matched, the request is allowed.<br />Each origin supports Exact, Prefix, Suffix, and RegularExpression matching. |
855+
| `additionalOrigins` | _[Origin](#origin) array_ | false | | AdditionalOrigins specifies additional origins that are allowed to make mutating<br />requests, beyond the destination origin. A request whose Origin header matches one<br />of them is allowed. The value "*" allows any origin, which effectively disables<br />origin validation.<br />Note: Envoy's CSRF filter compares the host and port of the origin only, so the<br />scheme is ignored: "https://www.example.com" and "http://www.example.com" are<br />equivalent here, and both allow the request regardless of the scheme the client<br />used. |
861856

862857

863858
#### CircuitBreaker
@@ -4509,6 +4504,7 @@ For example, the following are valid origins:
45094504

45104505
_Appears in:_
45114506
- [CORS](#cors)
4507+
- [CSRF](#csrf)
45124508

45134509

45144510

@@ -6112,7 +6108,6 @@ This is a general purpose match condition that can be used by other EG APIs
61126108
that need to match against a string.
61136109

61146110
_Appears in:_
6115-
- [CSRF](#csrf)
61166111
- [HTTP1Settings](#http1settings)
61176112
- [HTTPHeaderFilter](#httpheaderfilter)
61186113
- [OIDCDenyRedirectHeader](#oidcdenyredirectheader)

0 commit comments

Comments
 (0)