KEVWire

Security intelligence, with context.

Response snapshot Oct 29, 2026, 01:26 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-118656

Echo has Open Redirect via Control-Character Prefix Defeating sanitizeURI

Echo's dedicated open-redirect guard, sanitizeURI, decides whether a redirect target is a local path by inspecting exactly two byte positions: uri[0] and uri[1].

At a glance

Exploited
Not known
Severity
6.1 Medium (CVSS 3.1)
Chance of exploitation
Not estimated
Sources
1 source

What to do

GitHub lists first patched versions: github.com/labstack/echo/v5: 5.4.0; github.com/labstack/echo/v4: 4.16.0. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-v753-g4cw-jm48 (opens a new tab)

What happened

Summary

Echo's dedicated open-redirect guard, sanitizeURI, decides whether a redirect target is a local path by inspecting exactly two byte positions: uri[0] and uri[1]. If the value begins with a control character such as a tab or a newline, followed by a separator pair, the guard does not fire and the value is written to the Location header unchanged.

Browsers remove tab and newline characters from URLs before parsing them, and treat a backslash as a forward slash for HTTP and HTTPS URLs. A Location value of `/\t/attacker.example is therefore resolved by the browser as an absolute URL on attacker.example`. Confirmed end to end in a headless browser.

The guard

Continue reading

middleware/slash.go:144 (duplicated verbatim at echo.go:958):

func sanitizeURI(uri string) string {
 // double slash `\\`, `//` or even `\/` is absolute uri for browsers and by redirecting request to that uri
 // we are vulnerable to open redirect attack. so replace all slashes from the beginning with single slash
 if len(uri) > 1 && (uri[0] == '\\' || uri[0] == '/') && (uri[1] == '\\' || uri[1] == '/') {
 uri = "/" + strings.TrimLeft(uri, `/\`)
 }
 return uri
}

The condition is anchored at absolute offsets 0 and 1. It cannot see a separator that has been shifted by one byte.

Data flow

1. RemoveTrailingSlash (middleware/slash.go:112-140) reads url.Path, which net/http has already percent-decoded, strips the trailing slash, and appends the raw query string:

path := url.Path
...
if l > 0 && strings.HasSuffix(path, "/") {
 path = path[:l]
 uri := path
 if qs != "" {
 uri += "?" + qs
 }
 if config.RedirectCode != 0 {
 return c.Redirect(config.RedirectCode, sanitizeURI(uri)) // line 132
 }

2. AddTrailingSlash does the same in the opposite direction at middleware/slash.go:67.

3. A request for /%09/attacker.example/x/ has url.Path equal to "/\t/attacker.example/x/" (a literal TAB byte). sanitizeURI evaluates uri[0] == '\t', the condition is false, and the value passes through untouched.

4. Context.Redirect (context.go:735) writes it to the Location header. Go's header writer removes CR and LF but leaves TAB, which is all that is required.

Conditional analysis

- Guard: uri[0] and uri[1] are separators. Trust boundary: server-side sanitization of attacker-influenced path data. - Bypass: place a single control byte before the separator pair. The separator moves to index 1 and the guard's second condition (uri[1] being a separator) is never reached because the first condition (uri[0] being a separator) already failed. - Browser-side trust boundary: the client's URL parser.

It is adversary favourable: control characters are stripped and backslashes are normalized before scheme and authority parsing. The attacker does not need any server-side weakness beyond producing the malformed Location.

Note that the newline variant does not survive: net/http replaces CR and LF in header values. TAB does survive, and TAB alone is sufficient because the browser strips it.

Proof of concept

Attached PoC: ECHO-2026-002_poc.zip (in this directory). Extract it and follow ECHO-2026-002/README.md for the full walkthrough. The archive contains a self-contained Go module (go.mod, go.sum, main.go, README.md).

[ECHO-2026-002_poc.zip](https://github.com/user-attachments/files/32133975/ECHO-2026-002_poc.zip)

Quick start:

unzip ECHO-2026-002_poc.zip
cd ECHO-2026-002
# edit go.mod: point the replace directive at your Echo checkout
sed -i 's|=> .*|=> /absolute/path/to/echo|' go.mod
GOFLAGS=-mod=mod go run . # in-process check
GOFLAGS=-mod=mod go run . -serve # real listener on 127.0.0.1:18002

Environment used for verification

- OS: Linux 6.18.33.2-microsoft-standard-WSL2, x86_64 - Go: go1.27.0 linux/amd64 - Target: Echo v5.3.1 (module github.com/labstack/echo/v5) - Browser: headless Chromium drove the end-to-end confirmation

The vulnerable configuration

package main

import (
 "net/http"

 "github.com/labstack/echo/v5"
 "github.com/labstack/echo/v5/middleware"
)

func main() {
 e := echo.New()
 e.Pre(middleware.RemoveTrailingSlashWithConfig(middleware.RemoveTrailingSlashConfig{
 RedirectCode: http.StatusMovedPermanently,
 }))
 e.GET("/", func(c *echo.Context) error { return c.String(200, "home") })
 _ = e.Start("127.0.0.1:18002")
}

***

Reproduction

In-process check. The program requests five path shapes and applies the browser URL-parsing steps that matter (strip TAB, LF, CR; treat a backslash as a slash; a leading // starts an authority):

GOFLAGS=-mod=mod go run .
GET code Location (control chars shown) browser navigates to
/normal/path/ 301 /normal/path (victim origin)
 control: ordinary path, sanitized correctly
/%09/attacker.example/x/ 301 /^I/attacker.example/x attacker.example
 exploit: TAB then slash
/%09%5Cattacker.example/x/ 301 /^I\attacker.example/x attacker.example
 exploit: TAB then backslash
/..%09/attacker.example/ 301 /..^I/attacker.example (victim origin)
 NOT exploitable: TAB is not at index 1
/%09//attacker.example/ 301 /^I//attacker.example (victim origin)
 NOT exploitable: three slashes is an empty authority

RESULT: reproducible. ...

^I is a literal TAB byte. The last two rows are deliberate negatives, included so the mechanism (the guard reads only uri[0] and uri[1]) is unambiguous.

Over the wire, with the -serve variant running on 127.0.0.1:18002:

1. Show the malformed Location on the wire. No backslash is needed for this variant:

curl -s -i --max-time 5 'http://127.0.0.1:18002/%09/attacker.example/x/' | cat -A | head -3
# HTTP/1.1 301 Moved Permanently
# Location: /^I/attacker.example/x

cat -A renders the tab as ^I, confirming it is a literal byte in the header.

2. Show that LF does NOT work, which proves the mechanism is the guard's fixed byte offsets rather than generic header injection:

``

Affected package ranges reported by GitHub: github.com/labstack/echo/v5: < 5.4.0 github.com/labstack/echo/v4: < 4.16.0

Scores and how they were measured

CVSS base severity

6.1

Version 3.1As of

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

Measurement source (opens a new tab)

How bad it is if exploited. Not a prediction of exploitation or proof you are exposed.

EPSS probability

Not available

A model's estimate of exploitation in the next 30 days, not certainty or your own risk.

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-v753-g4cw-jm48 (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-v753-g4cw-jm48, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 28). Echo has Open Redirect via Control-Character Prefix Defeating sanitizeURI (CVE-2026-118656). https://kevwire.com/record/CVE-2026-118656

MLA

"Echo has Open Redirect via Control-Character Prefix Defeating sanitizeURI (CVE-2026-118656)." KEVWire, 28 Oct. 2026, kevwire.com/record/CVE-2026-118656.

Chicago

KEVWire. "Echo has Open Redirect via Control-Character Prefix Defeating sanitizeURI (CVE-2026-118656)." Last modified October 28, 2026. https://kevwire.com/record/CVE-2026-118656.

Evidence summary (plain text)

Echo has Open Redirect via Control-Character Prefix Defeating sanitizeURI (Oct 28, 2026).
Echo's dedicated open-redirect guard, sanitizeURI, decides whether a redirect target is a local path by inspecting exactly two byte positions: uri[0] and uri[1]. If the value begins with a control character such as a tab or a newline, followed by a separator pair, the guard does not fire and the value is written to the Location header unchanged.

Browsers remove tab and newline characters from URLs before parsing them, and treat a backslash as a forward slash for HTTP and HTTPS URLs.
CVSS 6.1 (version 3.1; evidence as of Oct 28, 2026; https://github.com/advisories/GHSA-v753-g4cw-jm48).
Sources:
GitHub-reviewed advisory GHSA-v753-g4cw-jm48: https://github.com/advisories/GHSA-v753-g4cw-jm48
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
Record ID: CVE-2026-118656.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 29, 2026, 00:55 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are coming as part of KEVWire Premium: join the waitlist. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail