{"id":"CVE-2026-117645","aliases":["EEF-CVE-2026-117645"],"title":"Phoenix.HTML vulnerable to attribute injection via unvalidated attribute names in attributes_escape/1","titleSource":"EEF","descriptionSource":"OSV","description":"## 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 `>`. 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 `<tag {@attrs}>`, `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 `&`, `<`, `>`, `\"` and `'`. Whitespace passes. HTML uses whitespace to separate attributes, so `%{\"x onmouseover\" => \"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\\\"><script>\", \"y\"}]` renders ` aria-x\"><script>=\"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 `<div {@attrs}>` 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\" => \"alert(1)\"}) |> 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\"}]) |> Phoenix.HTML.safe_to_string()` returns ` x\" onclick=\"alert(1)=\"y\"`.\n4. Render a nested atom name: `Phoenix.HTML.attributes_escape(aria: [{:\"x\\\"><script>\", \"y\"}]) |> Phoenix.HTML.safe_to_string()` returns ` aria-x\"><script>=\"y\"`.\n5. Place any of these maps in a HEEx template as `<div {@attrs}>` 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.","severity":"medium","score":5.3,"published":"2026-10-30T20:25:06.758Z","modified":"2026-10-30T20:30:03.351Z","withdrawn":false,"knownExploited":false,"affected":[{"ecosystem":"Hex","name":"phoenix_html","sources":[{"name":"OSV · EEF-CVE-2026-117645","url":"https://osv.dev/vulnerability/EEF-CVE-2026-117645"}],"ranges":[{"events":[{"introduced":"1.0.0"},{"fixed":"3.3.5"}],"type":"SEMVER"},{"events":[{"introduced":"4.0.0"},{"fixed":"4.4.0"}],"type":"SEMVER"}],"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"],"fixed":["3.3.5","4.4.0"]}],"sourceAffected":[{"ranges":[{"events":[{"introduced":"6ad3a1d7960891a468d54636054a98e11dfb5111"},{"fixed":"d9e4e6b2d9be49e25fb6d6b4fdbb2008174f01a6"},{"fixed":"a47dc8944a773b7e9273d9691dc6744ec793f1de"}],"type":"GIT","repo":"https://github.com/phoenixframework/phoenix_html"}],"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"]}],"references":["https://cna.erlef.org/cves/CVE-2026-117645.html","https://osv.dev/vulnerability/EEF-CVE-2026-117645","https://github.com/phoenixframework/phoenix_html/commit/6ad3a1d7960891a468d54636054a98e11dfb5111","https://github.com/phoenixframework/phoenix_html/commit/d9e4e6b2d9be49e25fb6d6b4fdbb2008174f01a6","https://github.com/phoenixframework/phoenix_html/commit/a47dc8944a773b7e9273d9691dc6744ec793f1de","https://hex.pm/packages/phoenix_html"],"sources":[{"name":"CVE","id":"CVE-2026-117645","modified":"2026-10-30T20:25:06.758Z","url":"https://www.cve.org/CVERecord?id=CVE-2026-117645"},{"name":"OSV","id":"EEF-CVE-2026-117645","modified":"2026-10-30T20:30:03.351618265Z","url":"https://osv.dev/vulnerability/EEF-CVE-2026-117645"}],"assessments":[{"source":"EEF","vector":"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","score":5.3},{"source":"OSV","vector":"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","score":5.3}],"cve":{"records":[{"id":"CVE-2026-117645","source":"EEF","state":"PUBLISHED","reasons":[],"replacements":[],"disputed":false,"unsupportedWhenAssigned":false,"reserved":"2026-10-30T12:15:02.726Z","published":"2026-10-30T20:25:06.758Z","modified":"2026-10-30T20:25:06.758Z"}],"publishers":[{"name":"EEF","role":"CNA","organizationId":"6b3ad84c-e1a6-4bf7-a703-f496b71e49db","modified":"2026-10-30T20:25:06.758Z"}],"references":[{"url":"https://cna.erlef.org/cves/CVE-2026-117645.html","tags":["related","third-party-advisory"],"source":"EEF"},{"url":"https://osv.dev/vulnerability/EEF-CVE-2026-117645","tags":["related"],"source":"EEF"},{"url":"https://github.com/phoenixframework/phoenix_html/commit/6ad3a1d7960891a468d54636054a98e11dfb5111","tags":["related"],"source":"EEF"},{"url":"https://github.com/phoenixframework/phoenix_html/commit/d9e4e6b2d9be49e25fb6d6b4fdbb2008174f01a6","tags":["patch"],"source":"EEF"},{"url":"https://github.com/phoenixframework/phoenix_html/commit/a47dc8944a773b7e9273d9691dc6744ec793f1de","tags":["patch"],"source":"EEF"}],"weaknesses":[{"id":"CWE-79","description":"CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')","source":"EEF"}],"applicability":[{"source":"EEF","statements":[{"nodes":[{"cpeMatch":[{"criteria":"cpe:2.3:a:phoenixframework:phoenix_html:*:*:*:*:*:*:*:*","versionEndExcluding":"3.3.5","versionStartIncluding":"1.0.0","vulnerable":true},{"criteria":"cpe:2.3:a:phoenixframework:phoenix_html:*:*:*:*:*:*:*:*","versionEndExcluding":"4.4.0","versionStartIncluding":"4.0.0","vulnerable":true}],"negate":false,"operator":"OR"}],"operator":"AND"}]}],"prioritization":[]},"products":[{"vendor":"phoenixframework","name":"phoenix_html","platforms":[],"versions":[{"version":"1.0.0","status":"affected","lessThan":"3.3.5","versionType":"semver"},{"version":"4.0.0","status":"affected","lessThan":"4.4.0","versionType":"semver"}],"defaultStatus":"unaffected","packageURL":"pkg:hex/phoenix_html","packageIdentity":{"ecosystem":"Hex","name":"phoenix_html"},"cpes":["cpe:2.3:a:phoenixframework:phoenix_html:*:*:*:*:*:*:*:*"],"source":"EEF","url":"https://www.cve.org/CVERecord?id=CVE-2026-117645"},{"vendor":"phoenixframework","name":"phoenix_html","platforms":[],"versions":[{"version":"6ad3a1d7960891a468d54636054a98e11dfb5111","status":"affected","lessThan":"*","versionType":"git","changes":[{"at":"d9e4e6b2d9be49e25fb6d6b4fdbb2008174f01a6","status":"unaffected"},{"at":"a47dc8944a773b7e9273d9691dc6744ec793f1de","status":"unaffected"}]}],"defaultStatus":"unaffected","packageURL":"pkg:github/phoenixframework/phoenix_html","cpes":["cpe:2.3:a:phoenixframework:phoenix_html:*:*:*:*:*:*:*:*"],"source":"EEF","url":"https://www.cve.org/CVERecord?id=CVE-2026-117645"}],"remediation":[{"text":"Do 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.","source":"EEF","url":"https://www.cve.org/CVERecord?id=CVE-2026-117645"}],"observedAt":"2026-10-30T23:52:25.671Z","kev":{"status":"not-listed","checkedAt":"2026-10-31T00:45:31.953Z"},"coverage":[{"source":"NVD","status":"available","checkedAt":"2026-10-30T21:55:36.698Z","attemptedAt":"2026-10-30T21:55:36.698Z"},{"source":"GHSA","status":"available","checkedAt":"2026-10-30T21:55:36.915Z","attemptedAt":"2026-10-30T21:55:36.915Z"},{"source":"EUVD","status":"no-result","checkedAt":"2026-10-30T21:55:38.428Z","attemptedAt":"2026-10-30T21:55:38.428Z"},{"source":"EPSS","status":"no-result","checkedAt":"2026-10-30T21:55:38.068Z","attemptedAt":"2026-10-30T21:55:38.068Z"}],"evidence":[{"source":"NVD","id":"CVE-2026-117645","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-117645","modified":"2026-10-30T21:16:51.170","score":5.3,"vector":"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","checkedAt":"2026-10-30T21:55:36.698Z"},{"source":"GitHub Advisory Database","id":"GHSA-jwcv-27cf-c7j5","url":"https://github.com/advisories/GHSA-jwcv-27cf-c7j5","modified":"2026-10-30T21:32:49Z","checkedAt":"2026-10-30T21:55:36.915Z"}],"aiSummary":null}