Am I affected?

type your phoenix_html version to check

Description

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.

Technical analysis

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.

Weaknesses & attack patterns

Weakness

CWE-79 · Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') in catalog → MITRE ↗

Attack patterns

CAPEC-63 · Cross-Site Scripting (XSS) MITRE ↗

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.

Affected — Hex / phoenix_html Hex.pm ↗ Repository ↗

≥ 1.0.0 < 3.3.5 affected
≥ 4.0.0 < 4.4.0 affected
every other version: unaffected
cpe cpe:2.3:a:phoenixframework:phoenix_html:*:*:*:*:*:*:*:*
version type semver
modules · source files · routines
modules 'Elixir.Phoenix.HTML' · 'Elixir.Phoenix.HTML.Tag'
source files lib/phoenix_html.ex · lib/phoenix_html/tag.ex
routines 'Elixir.Phoenix.HTML':attributes_escape/1 · 'Elixir.Phoenix.HTML':build_attrs/1 · 'Elixir.Phoenix.HTML':nested_attrs/3 · 'Elixir.Phoenix.HTML':key_escape/1 · 'Elixir.Phoenix.HTML.Tag':tag/2 · 'Elixir.Phoenix.HTML.Tag':content_tag/3 · 'Elixir.Phoenix.HTML.Tag':attributes_escape/1 · 'Elixir.Phoenix.HTML.Tag':tag_attrs/1

Affected — GitHub / phoenixframework/phoenix_html Repository ↗

≥ 6ad3a1d and up affected
→ d9e4e6b not affected
→ a47dc89 not affected
every other version: unaffected
cpe cpe:2.3:a:phoenixframework:phoenix_html:*:*:*:*:*:*:*:*
version type git
modules · source files · routines
modules 'Elixir.Phoenix.HTML' · 'Elixir.Phoenix.HTML.Tag'
source files lib/phoenix_html.ex · lib/phoenix_html/tag.ex
routines 'Elixir.Phoenix.HTML':attributes_escape/1 · 'Elixir.Phoenix.HTML':build_attrs/1 · 'Elixir.Phoenix.HTML':nested_attrs/3 · 'Elixir.Phoenix.HTML':key_escape/1 · 'Elixir.Phoenix.HTML.Tag':tag/2 · 'Elixir.Phoenix.HTML.Tag':content_tag/3 · 'Elixir.Phoenix.HTML.Tag':attributes_escape/1 · 'Elixir.Phoenix.HTML.Tag':tag_attrs/1

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.

References

Credits

Andrew Moore Finder Reporter
Steffen Deusch Remediation developer
Jonatan Männchen / EEF Coordinator

CVSS breakdown

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
« All CVEs