eef-cve-2026-117645

CVSS 5.3 osv_hex
Description

## 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.

Timeline
Published
unknown
Last Modified
unknown
CVSS Details

CVSS details not available.

Affected Products

No product information available.

Weaknesses (CWE)
References
Linked Vulnerabilities

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"
}
View JSON API Download JSON