{"id":"CVE-2026-118652","aliases":["GHSA-h9g5-28mm-hx3g"],"title":"Echo vulnerable to Reflected Cross-Site Scripting (XSS) via unsanitized JSONP callback in Context.JSONP / Context.JSONPBlob","summary":"Echo vulnerable to Reflected Cross-Site Scripting (XSS) via unsanitized JSONP callback in Context.JSONP / Context.JSONPBlob","severity":"medium","cvss":6.1,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","vendor":"labstack","product":"github.com/labstack/echo/v5","ecosystem":"go","affected":["github.com/labstack/echo/v5 < 5.4.0","github.com/labstack/echo/v4 < 4.16.0"],"patched":["github.com/labstack/echo/v5 5.4.0","github.com/labstack/echo/v4 4.16.0"],"published":"2026-10-28","updated":"2026-10-28","sourceUpdated":"2026-10-28T20:15:05.451169812Z","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-h9g5-28mm-hx3g","references":[{"url":"https://github.com/labstack/echo/security/advisories/GHSA-h9g5-28mm-hx3g"},{"url":"https://github.com/labstack/echo/commit/b838391e2eade87ae7e5a09d7bb5087bec271ec1"},{"url":"https://github.com/labstack/echo/commit/fc410094b2f3415cf2bf5204082137b205bfd5af"},{"url":"https://github.com/labstack/echo"},{"url":"https://github.com/labstack/echo/releases/tag/v4.16.0"},{"url":"https://github.com/labstack/echo/releases/tag/v5.4.0"},{"url":"https://github.com/advisories/GHSA-h9g5-28mm-hx3g"}],"tags":["osv","go","ghsa"],"epss":0.00239,"epssPercentile":0.15025,"cwe":["CWE-79"],"ingestedAt":"2026-10-28T20:17:49.703Z","slug":"CVE-2026-118652","body":"## Overview\n\n### Summary\n\nThe `Context.JSONP`, `Context.JSONPBlob`, and internal `jsonPBlob` helper in `context.go` directly concatenate the caller-supplied `callback` parameter into a JavaScript response **without any validation, escaping, or allow-listing**.  \n\nThis creates a high-impact **reflected XSS** that can be triggered by any route passing attacker-controlled data (query string, form, header, path param, etc.) into the callback.\n\n**Present in current master and v5.1.0 (latest release).**\n\n### Vulnerable Code (verbatim)\n\n```go\nfunc (c *Context) jsonPBlob(code int, callback string, i any) (err error) {\n\tc.writeContentType(MIMEApplicationJavaScriptCharsetUTF8)\n\tc.response.WriteHeader(code)\n\tif _, err = c.response.Write([]byte(callback + \"(\")); err != nil {\n\t\treturn\n\t}\n\tif err = c.echo.JSONSerializer.Serialize(c, i, \"\"); err != nil {\n\t\treturn\n\t}\n\tif _, err = c.response.Write([]byte(\");\")); err != nil {\n\t\treturn\n\t}\n\treturn\n}\n\nfunc (c *Context) JSONP(code int, callback string, i any) (err error) {\n\treturn c.jsonPBlob(code, callback, i)\n}\n\nfunc (c *Context) JSONPBlob(code int, callback string, b []byte) (err error) {\n\tc.writeContentType(MIMEApplicationJavaScriptCharsetUTF8)\n\tc.response.WriteHeader(code)\n\tif _, err = c.response.Write([]byte(callback + \"(\")); err != nil {\n\t\treturn\n\t}\n\tif _, err = c.response.Write(b); err != nil {\n\t\treturn\n\t}\n\t_, err = c.response.Write([]byte(\");\"))\n\treturn\n}\n```\nNo sanitization is performed on the callback string.\n\n### Exploit (one-request repro)\nAny route using the official pattern is vulnerable:\n```\nGoe.GET(\"/api\", func(c echo.Context) error {\n    return c.JSONP(200, c.QueryParam(\"callback\"), map[string]string{\"status\": \"ok\"})\n})\n```\nAttacker request:\n```\nGET /api?callback=alert(document.domain)%2f%2f HTTP/1.1\nHost: target.com\n```\nServer response:\n```\nContent-Type: application/javascript; charset=utf-8\n\nalert(document.domain)//({\"status\":\"ok\"});\n```\n→ Immediate arbitrary JavaScript execution (full reflected XSS).\n### Impact\n\n- Session hijacking, cookie theft, account takeover, keylogging, defacement.\n- Works even with strict CSP (response is served as application/javascript).\n\n#### Affected Versions\n\n- All versions ≤ v5.1.0 (including current master)\n\n#### Workarounds\nManually sanitize the callback before calling JSONP/JSONPBlob:\n```\nGofunc safeCallback(cb string) string {\n    if matched, _ := regexp.MatchString(`^[a-zA-Z0-9_.\\[\\]]+$`, cb); !matched {\n        return \"callback\"\n    }\n    return cb\n}\n```\nOr migrate away from JSONP entirely (use CORS + application/json).\n### References\n\n- https://github.com/labstack/echo/blob/master/context.go\n- https://echo.labstack.com/docs/cookbook/jsonp\n\n## Patches\n\nFixed in `github.com/labstack/echo/v5` v5.4.0 and `github.com/labstack/echo/v4` v4.16.0. `Context.JSONP` and `Context.JSONPBlob` accept only a callback that is empty, a JavaScript identifier or a dot-separated path of identifiers (ASCII letters, digits, `_` and `$`); any other callback returns a 400 Bad Request error wrapping `ErrInvalidJSONPCallback` and nothing is written. JSONP responses also carry `X-Content-Type-Options: nosniff`.\n\n## Workarounds\n\nValidate the callback in your handler before calling `Context.JSONP`, for example against `^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$`, and set `X-Content-Type-Options: nosniff`. JSONP responses can be read by any website with the user's cookies, so do not serve data that requires authentication through JSONP.\n\n## Affected packages\n\n- `github.com/labstack/echo/v5 < 5.4.0`\n- `github.com/labstack/echo/v4 < 4.16.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `github.com/labstack/echo/v5 5.4.0`\n- `github.com/labstack/echo/v4 4.16.0`","depth":"sunlit","depthScore":34,"depthScoreParts":{"impact":33.6,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}