eef-cve-2026-117645
CVSS 5.3 osv_hex## Summary Improper 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`. `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`. This issue affects phoenix_html: from 1.0.0 before 3.3.5 and from 4.0.0 before 4.4.0. ## Details **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. **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. **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. **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. ## Proof of concept 1. Install a vulnerable release, for example `Mix.install([{:phoenix_html, "== 4.3.0"}])`. 2. 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)"`. 3. 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"`. 4. Render a nested atom name: `Phoenix.HTML.attributes_escape(aria: [{:"x\"><script>", "y"}]) |> Phoenix.HTML.safe_to_string()` returns ` aria-x"><script>="y"`. 5. Place any of these maps in a HEEx template as `<div {@attrs}>` to deliver the injected attribute to a browser. ## Impact An 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. ## Workarounds 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.
- Published
- unknown
- Last Modified
- unknown
CVSS details not available.
No product information available.
- https://cna.erlef.org/cves/CVE-2026-117645.html
- 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
No linked vulnerabilities found.
{
"affected": [
{
"database_specific": {
"source": "https://cna.erlef.org/osv/EEF-CVE-2026-117645.json"
},
"package": {
"ecosystem": "Hex",
"name": "phoenix_html",
"purl": "pkg:hex/phoenix_html"
},
"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"
]
},
{
"database_specific": {
"source": "https://cna.erlef.org/osv/EEF-CVE-2026-117645.json"
},
"ranges": [
{
"events": [
{
"introduced": "6ad3a1d7960891a468d54636054a98e11dfb5111"
},
{
"fixed": "d9e4e6b2d9be49e25fb6d6b4fdbb2008174f01a6"
},
{
"fixed": "a47dc8944a773b7e9273d9691dc6744ec793f1de"
}
],
"repo": "https://github.com/phoenixframework/phoenix_html",
"type": "GIT"
}
],
"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"
]
}
],
"aliases": [
"CVE-2026-117645"
],
"credits": [
{
"name": "Andrew Moore",
"type": "FINDER"
},
{
"name": "Andrew Moore",
"type": "REPORTER"
},
{
"name": "Steffen Deusch",
"type": "REMEDIATION_DEVELOPER"
},
{
"name": "Jonatan Männchen / EEF",
"type": "COORDINATOR"
}
],
"database_specific": {
"capec_ids": [
"CAPEC-63"
],
"cpe_ids": [
"cpe:2.3:a:phoenixframework:phoenix_html:*:*:*:*:*:*:*:*"
],
"cwe_ids": [
"CWE-79"
]
},
"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 `>`. 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.",
"id": "EEF-CVE-2026-117645",
"modified": "2026-10-30T20:30:03.351618265Z",
"published": "2026-10-30T20:25:06.758Z",
"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"
}
],
"schema_version": "1.9.2",
"severity": [
{
"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",
"type": "CVSS_V4"
}
],
"summary": "Phoenix.HTML vulnerable to attribute injection via unvalidated attribute names in attributes_escape/1"
}