{"id":"CVE-2026-118652","kind":"vulnerability","aliases":["CVE-2026-118652","GHSA-h9g5-28mm-hx3g"],"related":[],"title":"Echo vulnerable to Reflected Cross-Site Scripting (XSS) via unsanitized JSONP callback in Context.JSONP / Context.JSONPBlob","description":"### 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.","summary":"Echo's Context.JSONP and Context.JSONPBlob write the caller-supplied callback name straight into the JavaScript response without validation. An attacker who gets a victim to open a crafted link runs arbitrary JavaScript in the site's origin, which can steal session cookies or act as the user.","severity":"medium","cvss":6.1,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"github.com/labstack/echo/v5","fixed":["5.4.0"],"affected":"< 5.4.0","ecosystem":"go"},{"name":"github.com/labstack/echo/v4","fixed":["4.16.0"],"affected":"< 4.16.0","ecosystem":"go"}],"ecosystems":["go"],"tags":["api-servers","cwe-79","echo","go","package","self-hosted"],"not_affected_if":["fixed-version","no-jsonp-routes","static-callback"],"how_to_check":"- Run `go list -m github.com/labstack/echo/v4` or `go list -m github.com/labstack/echo/v5` and compare against `4.16.0` and `5.4.0`\n- Search the codebase for `JSONP(` and `JSONPBlob(` calls that pass request input as the callback\n- Check whether any JSONP route returns authenticated data","fix":"- Upgrade echo v5 to `5.4.0` or echo v4 to `4.16.0`\n- Until upgraded, validate the callback against `^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$` before calling JSONP\n- Set `X-Content-Type-Options: nosniff` on JSONP responses\n- Stop serving authenticated data through JSONP and use CORS with application/json instead","status":"open","published_at":"2026-10-28T19:54:05+00:00","first_seen_at":"2026-10-28T19:56:29+00:00","triaged_at":"2026-10-28T19:56:38+00:00","updated_at":"2026-10-28T19:56:38+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-10-28T19:56:38Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade echo v5 to `5.4.0` or echo v4 to `4.16.0`\n- Until upgraded, validate the callback against `^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$` before calling JSONP\n- Set `X-Content-Type-Options: nosniff` on JSONP responses\n- Stop serving authenticated data through JSONP and use CORS with application/json instead","reach":"internet","title":"Echo: JSONP callback parameter reflects unescaped JavaScript, enabling cross-site scripting","impact":"medium","summary":"Echo's Context.JSONP and Context.JSONPBlob write the caller-supplied callback name straight into the JavaScript response without validation. An attacker who gets a victim to open a crafted link runs arbitrary JavaScript in the site's origin, which can steal session cookies or act as the user.","category":"package","nicknames":[],"who_cares":["go","echo","api-servers","self-hosted"],"confidence":0.85,"needs_auth":"no","worst_case":"attacker runs JavaScript in victim's session, stealing cookies","analyst_note":"","how_to_check":"- Run `go list -m github.com/labstack/echo/v4` or `go list -m github.com/labstack/echo/v5` and compare against `4.16.0` and `5.4.0`\n- Search the codebase for `JSONP(` and `JSONPBlob(` calls that pass request input as the callback\n- Check whether any JSONP route returns authenticated data","needs_analyst":false,"not_affected_if":["fixed-version","no-jsonp-routes","static-callback"],"who_is_affected":"Applications built on echo v4 below 4.16.0 or echo v5 below 5.4.0 that call Context.JSONP or Context.JSONPBlob with a callback taken from the request (query string, form, header or path). Routes that use a fixed callback name or do not use JSONP are not exposed. Teams on the fixed versions can skip it.","user_interaction":"yes","needs_special_setup":"no","severity_if_unknown":"unknown"},"cost_usd":0.000857,"input_tokens":4808,"output_tokens":752,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-118652':19A,27A '-2026':18A,26A '-79':38B '-8':314C '/api':279C,297C '/docs/cookbook/jsonp':395C '/labstack/echo/blob/master/context.go':392C '/labstack/echo/v4':33A,406C '/labstack/echo/v5':30A,401C '200':286C '28mm':23A '2f':301C,302C '400':438C '9':373C,479C,488C 'a-za-z':471C,480C 'a-za-z0':369C,475C,484C 'accept':411C 'account':331C 'act':89B 'affect':345C 'alert':299C,315C 'allow':121C 'allow-list':120C 'also':450C 'api':35B 'api-serv':34B 'application/javascript':311C,344C 'application/json':388C 'arbitrari':76B,320C 'ascii':429C 'attack':65B,140C,294C 'attacker-control':139C 'authent':517C 'away':382C 'b':230C,249C 'bad':439C 'byte':184C,200C,231C,241C,255C 'c':166C,192C,206C,223C,281C 'c.echo.jsonserializer.serialize':191C 'c.jsonp':285C 'c.jsonpblob':218C 'c.queryparam':287C 'c.response.write':183C,199C,240C,248C,254C 'c.response.writeheader':179C,236C 'c.writecontenttype':177C,234C 'call':359C,466C 'callback':13A,55B,109C,152C,171C,185C,211C,220C,228C,242C,263C,288C,298C,357C,377C,414C,435C,461C 'caller':53B,107C 'caller-suppli':52B,106C 'carri':451C 'cb':363C,374C,379C 'charset':312C 'code':162C,169C,180C,209C,219C,226C,237C 'concaten':104C 'content':309C,454C,493C 'content-typ':308C 'context':167C,207C,224C 'context.go':102C 'context.jsonp':15A,47B,95C,408C,467C 'context.jsonpblob':16A,49B,96C,410C 'control':141C 'cooki':87B,329C,509C 'cor':387C 'craft':73B 'creat':124C 'cross':6A 'cross-sit':5A 'csp':339C 'current':155C,351C 'cve':17A,25A 'cwe':37B 'data':142C,514C 'defac':334C 'digit':431C 'direct':103C 'document.domain':300C,316C 'dot':424C 'dot-separ':423C 'echo':1A,39B,45B 'echo.context':282C 'echo.labstack.com':394C 'echo.labstack.com/docs/cookbook/jsonp':393C 'empti':417C 'entir':385C 'err':175C,182C,186C,190C,194C,198C,201C,215C,232C,239C,243C,247C,250C,253C 'errinvalidjsonpcallback':443C 'error':176C,216C,233C,283C,441C 'escap':118C 'etc':149C 'even':336C 'exampl':469C 'execut':322C 'exploit':265C 'fix':397C 'form':145C 'full':323C 'func':165C,205C,222C,280C 'get':67B,296C 'ghsa':21A 'ghsa-h9g5-28mm-hx3g':20A 'github.com':29A,32A,391C,400C,405C 'github.com/labstack/echo/blob/master/context.go':390C 'github.com/labstack/echo/v4':31A,404C 'github.com/labstack/echo/v5':28A,399C 'go':40B,164C 'goe.get':278C 'gofunc':361C 'h9g5':22A 'handler':464C 'header':146C 'helper':100C 'high':127C 'high-impact':126C 'hijack':328C 'host':44B,304C 'http/1.1':303C 'hx3g':24A 'identifi':420C,428C 'immedi':319C 'impact':128C,326C 'includ':350C 'int':170C,210C,227C 'intern':98C 'javascript':60B,77B,113C,321C,419C 'jsonp':12A,208C,384C,448C,497C,519C 'jsonp/jsonpblob':360C 'jsonpblob':99C,168C,225C 'keylog':333C 'latest':159C 'letter':430C 'link':74B 'list':122C 'manual':354C 'map':289C 'master':156C,352C 'match':367C,375C 'migrat':381C 'mimeapplicationjavascriptcharsetutf8':178C,235C 'name':56B 'nil':187C,195C,202C,244C,251C 'nosniff':457C,496C 'noth':445C 'offici':274C 'ok':293C,318C 'one':267C 'one-request':266C 'open':71B 'option':456C,495C 'origin':82B 'packag':41B 'param':148C 'paramet':110C 'pass':138C 'patch':396C 'path':147C,426C 'pattern':275C 'perform':260C 'present':153C 'queri':143C 'read':501C 'refer':389C 'reflect':4A,129C,324C 'regexp.matchstring':368C 'releas':160C 'repro':269C 'request':268C,295C,440C 'requir':516C 'respons':61B,114C,307C,340C,449C,498C 'return':188C,196C,203C,204C,217C,245C,252C,256C,284C,376C,378C,436C 'rout':137C,271C 'run':75B 'safecallback':362C 'sanit':258C,355C 'script':8A 'self':43B 'self-host':42B 'separ':425C 'serv':342C,513C 'server':36B,306C 'session':86B,327C 'set':490C 'site':7A,80B 'status':292C,317C 'steal':85B 'straight':57B 'strict':338C 'string':144C,172C,212C,229C,264C,290C,291C,364C,365C 'summari':93C 'suppli':54B,108C 'takeov':332C 'target.com':305C 'theft':330C 'trigger':134C 'type':310C,455C,494C 'unsanit':11A 'use':272C,386C 'user':92B,507C 'utf':313C 'v4.16.0':407C 'v5.1.0':158C,349C 'v5.4.0':402C 'valid':63B,117C,459C 'verbatim':163C 'version':346C,348C 'via':10A 'victim':69B 'vulner':2A,161C,277C 'websit':504C 'without':62B,115C 'work':335C 'workaround':353C,458C 'wrap':442C 'write':50B 'written':447C 'x':453C,492C 'x-content-type-opt':452C,491C 'xss':9A,130C,325C 'z':474C,483C 'z0':372C,478C,487C 'za':371C,473C,477C,482C,486C","nicknames":[],"mitigations":[],"who_affected":"Applications built on echo v4 below 4.16.0 or echo v5 below 5.4.0 that call Context.JSONP or Context.JSONPBlob with a callback taken from the request (query string, form, header or path). Routes that use a fixed callback name or do not use JSONP are not exposed. Teams on the fixed versions can skip it.","tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-28T19:54:05+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-h9g5-28mm-hx3g","url":"https://github.com/advisories/GHSA-h9g5-28mm-hx3g","severity":"medium","published_at":"2026-10-28T19:54:05+00:00","modified_at":"2026-10-28T19:54:06+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-118652","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-118652","severity":null,"published_at":"2026-10-28T20:17:22+00:00","modified_at":"2026-10-28T20:17:22+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"}