{"id":"EEF-CVE-2026-117645","summary":"Phoenix.HTML vulnerable to attribute injection via unvalidated attribute names in attributes_escape/1","details":"## Summary\n\nImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting') vulnerability in phoenixframework phoenix_html allows an attacker who controls attribute names to inject attributes and script into rendered HTML via `Phoenix.HTML.attributes_escape/1`.\n\n`attributes_escape/1` in `lib/phoenix_html.ex` escapes attribute values but not attribute names. `key_escape/1` passes a binary key through `html_escape`, which neutralizes quotes and angle brackets but not whitespace, so a key such as `x onmouseover` ends the attribute and starts a new one. An atom key is only dasherized, so any character passes, including quotes and `\u003e`. Nested `data`, `aria` and `phx` keys take the same paths. An application is affected when it builds attribute keys from untrusted input and renders them through HEEx `\u003ctag {@attrs}\u003e`, `content_tag/3`, or a direct call. Before 3.0.0 the same code lived in `Phoenix.HTML.Tag`.\n\nThis issue affects phoenix_html: from 1.0.0 before 3.3.5 and from 4.0.0 before 4.4.0.\n\n## Details\n\n**1. Attribute rendering.** `Phoenix.HTML.attributes_escape/1` walks the key-value pairs through `build_attrs/1` and `nested_attrs/3` and emits each as `key_escape(k)=\"attr_escape(v)\"`. Values are escaped. Names are not checked against the HTML attribute-name grammar.\n\n**2. Binary names.** `key_escape/1` sends a binary through `Phoenix.HTML.Engine.html_escape/1`, which replaces `&`, `\u003c`, `\u003e`, `\"` and `'`. Whitespace passes. HTML uses whitespace to separate attributes, so `%{\"x onmouseover\" =\u003e \"alert(1)\"}` renders ` x onmouseover=\"alert(1)\"`: a harmless `x` attribute followed by an attacker-chosen event handler.\n\n**3. Atom names.** `key_escape/1` converts an atom with `Atom.to_string/1` and replaces `_` with `-`. Nothing is escaped, so `[{:\"x\\\" onclick=\\\"alert(1)\", \"y\"}]` renders ` x\" onclick=\"alert(1)=\"y\"` and `aria: [{:\"x\\\"\u003e\u003cscript\u003e\", \"y\"}]` renders ` aria-x\"\u003e\u003cscript\u003e=\"y\"`, which leaves the tag entirely. This path needs the application to create atoms from untrusted input, for example `Jason.decode!(body, keys: :atoms)` or `String.to_atom/1` on a parameter.\n\n**4. Reachability.** HEEx compiles `\u003cdiv {@attrs}\u003e` to a call to `Phoenix.HTML.attributes_escape/1` through `Phoenix.LiveView.HTMLEngine` and `Phoenix.LiveView.TagEngine`. `tag/2` and `content_tag/3` in `Phoenix.HTML.Tag` (phoenix_html 1.x to 3.x) and in `PhoenixHTMLHelpers.Tag` (4.x) render attributes the same way. Versions before 3.0.0 emitted the dasherized name without any escaping at all.\n\n## Proof of concept\n\n1. Install a vulnerable release, for example `Mix.install([{:phoenix_html, \"== 4.3.0\"}])`.\n2. Render a binary name containing a space: `Phoenix.HTML.attributes_escape(%{\"x onmouseover\" =\u003e \"alert(1)\"}) |\u003e Phoenix.HTML.safe_to_string()` returns ` x onmouseover=\"alert(1)\"`.\n3. Render an atom name containing a quote: `Phoenix.HTML.attributes_escape([{:\"x\\\" onclick=\\\"alert(1)\", \"y\"}]) |\u003e Phoenix.HTML.safe_to_string()` returns ` x\" onclick=\"alert(1)=\"y\"`.\n4. Render a nested atom name: `Phoenix.HTML.attributes_escape(aria: [{:\"x\\\"\u003e\u003cscript\u003e\", \"y\"}]) |\u003e Phoenix.HTML.safe_to_string()` returns ` aria-x\"\u003e\u003cscript\u003e=\"y\"`.\n5. Place any of these maps in a HEEx template as `\u003cdiv {@attrs}\u003e` to deliver the injected attribute to a browser.\n\n## Impact\n\nAn attacker who can influence the attribute names an application renders can add event-handler attributes or close the tag and run script in the browser of every user who views the page.\n\n## Workarounds\n\nDo not derive attribute names from untrusted input. Where dynamic attribute keys are needed, check each key against an allow list, or against a pattern such as `^[a-zA-Z][a-zA-Z0-9:._-]*$`, before passing the map to `attributes_escape/1` or a HEEx `{@attrs}` expression. Do not create atoms from request data.","aliases":["CVE-2026-117645"],"modified":"2026-10-30T20:30:03.351618265Z","published":"2026-10-30T20:25:06.758Z","database_specific":{"cwe_ids":["CWE-79"],"capec_ids":["CAPEC-63"],"cpe_ids":["cpe:2.3:a:phoenixframework:phoenix_html:*:*:*:*:*:*:*:*"]},"references":[{"type":"WEB","url":"https://cna.erlef.org/cves/CVE-2026-117645.html"},{"type":"WEB","url":"https://github.com/phoenixframework/phoenix_html/commit/6ad3a1d7960891a468d54636054a98e11dfb5111"},{"type":"FIX","url":"https://github.com/phoenixframework/phoenix_html/commit/d9e4e6b2d9be49e25fb6d6b4fdbb2008174f01a6"},{"type":"FIX","url":"https://github.com/phoenixframework/phoenix_html/commit/a47dc8944a773b7e9273d9691dc6744ec793f1de"},{"type":"PACKAGE","url":"https://hex.pm/packages/phoenix_html"}],"affected":[{"package":{"name":"phoenix_html","ecosystem":"Hex","purl":"pkg:hex/phoenix_html"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.0.0"},{"fixed":"3.3.5"}]},{"type":"SEMVER","events":[{"introduced":"4.0.0"},{"fixed":"4.4.0"}]}],"versions":["1.0.0","1.0.1","1.1.0","1.2.0","1.2.1","1.3.0","1.4.0","2.0.0","2.0.0-dev","2.0.1","2.1.0","2.1.1","2.1.2","2.10.0","2.10.1","2.10.2","2.10.3","2.10.4","2.10.5","2.11.0","2.11.1","2.11.2","2.12.0","2.13.0","2.13.1","2.13.2","2.13.3","2.13.4","2.14.0","2.14.1","2.14.2","2.14.3","2.2.0","2.3.0","2.3.1","2.4.0","2.4.0-dev","2.5.0","2.5.1","2.6.0","2.6.1","2.6.2","2.7.0","2.7.0-dev","2.8.0","2.9.0","2.9.1","2.9.2","2.9.3","3.0.0","3.0.1","3.0.2","3.0.3","3.0.4","3.1.0","3.2.0","3.3.0","3.3.1","3.3.2","3.3.3","3.3.4","4.0.0","4.1.0","4.1.1","4.2.0","4.2.1","4.3.0"],"database_specific":{"source":"https://cna.erlef.org/osv/EEF-CVE-2026-117645.json"}},{"ranges":[{"type":"GIT","repo":"https://github.com/phoenixframework/phoenix_html","events":[{"introduced":"6ad3a1d7960891a468d54636054a98e11dfb5111"},{"fixed":"d9e4e6b2d9be49e25fb6d6b4fdbb2008174f01a6"},{"fixed":"a47dc8944a773b7e9273d9691dc6744ec793f1de"}]}],"versions":["v3.3.4","v4.3.0","v4.2.1","v4.2.0","v3.3.3","v4.1.1","v4.1.0","v4.0.0","v3.3.2","v3.3.1","v3.3.0","v3.1.0","v3.0.4","v3.0.3","v3.0.2","v3.0.0","v2.14.2","v2.14.1","v.2.14.1","v2.14.0","v2.13.3","v2.13.2","v2.13.1","v2.13.0","v2.12.0","v2.11.2","v2.11.1","v2.11.0","v2.10.4","v2.10.2","v2.10.1","v2.10.0","v2.9.3","v2.9.2","v2.9.1","v2.9.0","v2.8.0","v2.7.0","v2.6.0","v2.5.1","v2.5.0","v2.4.0","v2.3.1","v2.3.0","v2.2.1","v2.2.0","v2.1.2","v2.1.0","v2.0.0","v1.4.0","v1.3.0","v1.2.1","v1.2.0","v1.1.0","v1.0.1","v1.0.0"],"database_specific":{"source":"https://cna.erlef.org/osv/EEF-CVE-2026-117645.json"}}],"schema_version":"1.9.2","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N"}],"credits":[{"name":"Andrew Moore","type":"FINDER"},{"name":"Andrew Moore","type":"REPORTER"},{"name":"Steffen Deusch","type":"REMEDIATION_DEVELOPER"},{"name":"Jonatan Männchen / EEF","type":"COORDINATOR"}]}