Skip to content

feat: add CSRF origin validation to SecurityPolicy - #8836

Open
albsga4 wants to merge 14 commits into
envoyproxy:mainfrom
albsga4:feat/securitypolicy-csrf-filter
Open

feat: add CSRF origin validation to SecurityPolicy#8836
albsga4 wants to merge 14 commits into
envoyproxy:mainfrom
albsga4:feat/securitypolicy-csrf-filter

Conversation

@albsga4

@albsga4 albsga4 commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?

api: add csrf field to SecurityPolicy

What this PR does / why we need it:

Envoy has a built-in envoy.filters.http.csrf filter that validates the Origin header against allowed origins on mutating requests (POST, PUT, DELETE, PATCH). Currently there's no way to configure this through Envoy Gateway without EnvoyPatchPolicy.

This PR adds native CSRF support following the exact same pattern as CORS:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: my-route
  csrf:
    additionalOrigins:
      - type: Suffix
        value: example.com

Implementation:

Follows the CORS pattern exactly:

  • API: SecurityPolicy.csrf with additionalOrigins []StringMatch (reuses existing StringMatch type)
  • IR: CSRF struct in SecurityFeatures with AdditionalOrigins []*StringMatch
  • Gateway API translation: buildCSRF() using irStringMatch() helper (same as CORS)
  • xDS: New csrf.go implementing httpFilter interface:
    • patchHCM: adds CSRF filter to listener disabled by default (0%)
    • patchRoute: enables CSRF per-route via typed_per_filter_config (100%)
    • patchResources: no-op

Behavior:

  • CSRF only checks mutating methods (POST, PUT, DELETE, PATCH) — GET/HEAD always pass
  • Origin is matched against the request destination AND any additionalOrigins
  • Missing Origin header on mutating requests → 403
  • Non-matching Origin → 403
  • Supports Exact, Prefix, Suffix, and RegularExpression matching

Changes:

  • api/v1alpha1/csrf_types.go (new) — CSRF API type
  • api/v1alpha1/securitypolicy_types.go — add CSRF *CSRF field
  • internal/ir/xds.go — CSRF IR struct in SecurityFeatures
  • internal/gatewayapi/securitypolicy.gobuildCSRF(), translation for route and gateway targets, TCP validation
  • internal/xds/translator/csrf.go (new) — httpFilter implementation
  • CRDs regenerated, deepcopy generated, API reference updated
  • Golden test: securitypolicy-with-csrf
  • Doc: site/content/en/latest/tasks/security/csrf.md
  • Release notes updated

Which issue(s) this PR fixes:

Fixes #8835

Release Notes: Yes

/cc @arkodg

@albsga4
albsga4 requested a review from a team as a code owner April 23, 2026 15:47
@netlify

netlify Bot commented Apr 23, 2026

Copy link
Copy Markdown

Deploy Preview for cerulean-figolla-1f9435 ready!

Name Link
🔨 Latest commit d349c42
🔍 Latest deploy log https://app.netlify.com/projects/cerulean-figolla-1f9435/deploys/6a6b71a607d7a20008f60b23
😎 Deploy Preview https://deploy-preview-8836--cerulean-figolla-1f9435.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@arkodg arkodg added this to the Backlog milestone Apr 26, 2026
@codecov

codecov Bot commented Apr 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.50847% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.82%. Comparing base (1912d0e) to head (d349c42).

Files with missing lines Patch % Lines
internal/xds/translator/csrf.go 73.07% 11 Missing and 10 partials ⚠️
internal/gatewayapi/securitypolicy.go 93.10% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8836      +/-   ##
==========================================
+ Coverage   75.80%   75.82%   +0.01%     
==========================================
  Files         258      259       +1     
  Lines       42795    42903     +108     
==========================================
+ Hits        32440    32530      +90     
- Misses       8176     8185       +9     
- Partials     2179     2188       +9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@albsga4
albsga4 force-pushed the feat/securitypolicy-csrf-filter branch 2 times, most recently from eb1feaf to d7ee907 Compare April 27, 2026 14:40
Comment thread charts/gateway-helm/templates/envoy-gateway-deployment.yaml
Comment thread test/config/envoy-gateaway-config/watch-namespaces.yaml
Comment thread api/v1alpha1/fault_injection.go
Comment thread internal/gatewayapi/backendtrafficpolicy.go Outdated
@zhaohuabing

Copy link
Copy Markdown
Member

Hi @albsga4 thanks for the contribution! Would you mind removing the changes that are unrelated to this PR, and adding an e2e test for CSRF?

@zhaohuabing
zhaohuabing requested a review from a team April 28, 2026 09:35
@albsga4
albsga4 force-pushed the feat/securitypolicy-csrf-filter branch 3 times, most recently from 826db28 to c6f1413 Compare April 28, 2026 17:12
@albsga4

albsga4 commented Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

Hi @zhaohuabing, thanks for the review!

Sorry about that — I had some test examples containing my company name that I needed to remove, and while doing that I wrongly rebased my fork which caused all those unrelated changes to leak into the PR.

I have now started fresh from latest main and the PR only contains CSRF-related files. I also added an e2e test following the CORS test pattern, covering all four StringMatch types (Exact, Prefix, Suffix, RegularExpression) with both allow and reject scenarios.

Let me know if there is anything else!

@albsga4
albsga4 requested a review from zhaohuabing April 28, 2026 17:28
@zhaohuabing

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16c2ded4f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/xds/translator/csrf.go
@albsga4

albsga4 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f859c44134

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread internal/xds/translator/httpfilters.go Outdated
@albsga4

albsga4 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@albsga4
albsga4 force-pushed the feat/securitypolicy-csrf-filter branch from d2055fd to ec0319f Compare April 30, 2026 19:56
@arkodg arkodg modified the milestones: Backlog, v1.9.0-rc.1 Release May 4, 2026
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had activity in the last 30 days. Please feel free to give a status update now, ping for review, when it's ready. Thank you for your contributions!

@github-actions github-actions Bot added the stale label Jun 3, 2026
@albsga4 albsga4 closed this Jun 3, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 878f37cd50

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/xds/translator/csrf.go
@zhaohuabing
zhaohuabing marked this pull request as draft July 28, 2026 02:38
@zhaohuabing
zhaohuabing force-pushed the feat/securitypolicy-csrf-filter branch from 878f37c to fc8c1ea Compare July 29, 2026 14:12
@zhaohuabing

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a3da5810e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

for _, origin := range csrf.AdditionalOrigins {
// Envoy's CSRF filter matches the host and port of the Origin header against the
// target host, so the scheme is dropped before the matcher is built.
hostAndPort := originHostAndPort(string(origin))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Canonicalize origins before building matchers

When additionalOrigins contains a valid but non-canonical URL such as https://EXAMPLE.com or https://example.com:443, browsers serialize the Origin host in lowercase and omit its default port, but this code only strips the scheme and builds a case-sensitive matcher for EXAMPLE.com or example.com:443. Such legitimate cross-origin requests are consequently rejected with 403 despite matching the configured origin; parse and canonicalize the host and port before constructing the matcher.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid observation, but pre-existing and not CSRF-specific. buildCORS builds its exact matcher straight from the configured value with no canonicalization either, so allowOrigins: [https://EXAMPLE.com] has behaved this way for a long time. additionalOrigins deliberately reuses the CORS Origin type, so canonicalizing only on the CSRF path would make two fields with the same type and the same docs behave differently.

I'd also argue the matcher is the wrong layer for it. Rewriting example.com:443example.com at translation time hides the misconfiguration rather than surfacing it. Better to reject it at admission on the shared Origin type, which covers CORS and CSRF together. Follow-up rather than this PR.

@zhaohuabing
zhaohuabing force-pushed the feat/securitypolicy-csrf-filter branch 2 times, most recently from 5386dba to b0741b1 Compare July 30, 2026 14:16
@zhaohuabing
zhaohuabing marked this pull request as ready for review July 30, 2026 14:23
@zhaohuabing
zhaohuabing force-pushed the feat/securitypolicy-csrf-filter branch from b0741b1 to 98f9340 Compare July 30, 2026 14:29
albsga4 and others added 13 commits July 30, 2026 08:01
Add a new CSRF field to SecurityPolicy that enables Envoy's CSRF filter
for cross-site request forgery protection. The filter validates the
Origin header on mutating requests (POST, PUT, DELETE, PATCH) against
the destination and any additional allowed origins.

Closes envoyproxy#8835

Signed-off-by: asalvador <asalvador@newrelic.com>
Add Prefix and RegularExpression to unit tests (gatewayapi + xds-ir),
and Prefix and Suffix to e2e test, so all four match types (Exact,
Prefix, Suffix, RegularExpression) are exercised at every test level.

Signed-off-by: asalvador <asalvador@newrelic.com>
Fix yamllint error: expected 6 but found 8 spaces indentation for
additionalOrigins list items.

Signed-off-by: asalvador <asalvador@newrelic.com>
Give the CSRF filter the same order as CORS (3) so it runs before
auth filters (ext_authz, JWT, etc.), rejecting cross-site mutating
requests early without invoking upstream auth logic.

Signed-off-by: asalvador <asalvador@newrelic.com>
Add envoy.filters.http.csrf to the kubebuilder validation enum so
filterOrder entries referencing CSRF pass CRD validation. Also update
the default filter order documentation to include CSRF.

Signed-off-by: asalvador <asalvador@newrelic.com>
… URLs

Envoy CSRF filter calls hostAndPort() on the Origin header before matching
against additional_origins, stripping the scheme. Values must be specified as
host or host:port (e.g., "www.example.com" not "https://www.example.com").

This follows the same approach as kgateway: pass values directly to Envoy
without transformation, and document the expected format clearly.

Updated all test fixtures and documentation to use host-only values.

Signed-off-by: asalvador <asalvador@newrelic.com>
Expose full Envoy CSRF filter capabilities:
- filterEnabled: percentage of requests with CSRF enforced (default 100%)
- shadowEnabled: percentage evaluated in dry-run mode (track but not enforce)
- additionalOrigins: values must be host:port (not full URLs)

Values are passed directly to Envoy without transformation. Envoy strips
the scheme from the Origin header before matching, so origins must be
specified as host or host:port.

Also fixes E2E test failures by using host-only origin values.

Changes:
- API: add FilterEnabled, ShadowEnabled fields to CSRF type
- IR: add fields to CSRF struct
- GatewayAPI: pass new fields in buildCSRF
- xDS: buildXdsCSRFPolicy uses percentage fields
- Golden tests: second route exercising filterEnabled/shadowEnabled
- E2E: shadow mode test (filterEnabled=0, shadowEnabled=100)
- Docs: shadow mode example and host:port format note

Signed-off-by: asalvador <asalvador@newrelic.com>
Signed-off-by: asalvador <asalvador@newrelic.com>
- Replace `filterEnabled`/`shadowEnabled` (`*int32`) with `enforcedFraction`/
  `shadowFraction` (`gwapiv1.Fraction`), matching how EG already models Envoy's
  RuntimeFractionalPercent (Tracing.SamplingFraction, RetryBudget.Percent), and
  translate them with the shared fractionalpercent helper.
- Give the CSRF filter its own explicit order instead of sharing CORS's, so it
  runs after cors (preflights are answered by the cors filter) and before the
  authn/authz filters.
- Preallocate the additionalOrigins slice to satisfy the prealloc linter.
- Regenerate CRDs, deepcopy, helm golden files, API reference and testdata.
- Move the release note to the new release-notes/current fragment layout so it
  lands in v1.9.0 instead of the already-cut v1.8.0-rc.1.

Signed-off-by: Huabing (Robin) Zhao <zhaohuabing@gmail.com>
Envoy strips the scheme from the Origin header before matching, and the header
never carries a path, so a value like `https://www.example.com` is accepted by
the CRD but can never match, silently 403ing legitimate requests. Add a CEL
rule on the CSRF type that rejects such values at admission, with cel-validation
coverage for the valid host / host:port forms and the scheme and path forms.

Also point the Gateway/HTTPRoute/GRPCRoute links in the CSRF task doc at
/reference/api-types/, matching the other task docs; the old /api-types/ paths
404 and broke docs-check-links.

Signed-off-by: Huabing (Robin) Zhao <zhaohuabing@gmail.com>
Partially enforcing an origin check is not a useful security posture: with
enforcedFraction below 100% a fraction of cross-site mutating requests is let
through unvalidated and unobserved. The legitimate use case behind the knob is
dry running the filter before turning it on, which shadowFraction already
covers, so drop enforcedFraction and make shadowFraction the single rollout
control.

Envoy only lets an invalid request through in shadow mode if that request was
not also selected by filter_enabled, so filter_enabled is now derived as the
complement of the shadow fraction. Every request is therefore either enforced
or observed, never neither, and a shadow fraction of 100% is a complete dry
run instead of a no-op.

Signed-off-by: Huabing (Robin) Zhao <zhaohuabing@gmail.com>
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>
Envoy draws filter_enabled and shadow_enabled independently, so a partial
shadow_enabled compounded with filter_enabled instead of partitioning with
it: a request selected by neither was left unenforced and unevaluated, never
reaching the csrf stats. Enforcement was skewed upwards too, since the filter
re-randomizes both predicates at each of its two call sites -- shadowFraction
90 rejected ~17% of invalid requests rather than 10%.

Pin shadow_enabled to 100% and let filter_enabled alone carry the rollout.
featureEnabled() short-circuits at 100% without touching the PRNG, so the
filter's first gate becomes unconditional -- every mutating request reaches
the stats block -- and the reject decision reduces to a single filter_enabled
draw. shadowFraction 40 now yields exactly 60% enforced and 40% shadowed,
with nothing unevaluated.

Signed-off-by: Huabing (Robin) Zhao <zhaohuabing@gmail.com>
@zhaohuabing
zhaohuabing force-pushed the feat/securitypolicy-csrf-filter branch from 98f9340 to bebe2c0 Compare July 30, 2026 15:02
Signed-off-by: Huabing (Robin) Zhao <huabing@tetrate.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add CSRF origin validation to SecurityPolicy

3 participants