Skip to content

JSON spliced into an attribute value breaks out of the attribute #507

Description

@allenap

Render for serde_json::Value (and Json<T>) writes JSON directly into the output buffer with no HTML escaping. That is correct for element content, but in an attribute value the JSON's own quotes terminate the attribute early, and the remainder is parsed as markup. An attacker who controls any string in the JSON therefore controls attributes on that element.

Reproduction

use maud::html;

let untrusted = "x onmouseover=alert(1) y";
let value = serde_json::json!({ "name": untrusted });
println!("{}", html! { div data-user=(value) {} }.into_string());

Output:

<div data-user="{"name":"x onmouseover=alert(1) y"}"></div>

An HTML tokenizer might read this as:

token value
data-user {
name":"x (empty)
onmouseover alert(1)
y"}" (empty)

onmouseover=alert(1) is a live event handler, injected from string data.

Impact

Any template that splices JSON into an attribute is vulnerable, e.g. the common pattern of passing server state to client-side JavaScript:

html! { div id="app" data-state=(state) {} }

Element content – including <script> tags, which the module is designed for – is not affected. The escaping of <, >, and & means the output can never open or close a tag there.

Affected APIs

  • impl Render for serde_json::Value
  • impl Render for Json<T>
  • impl Render for serde_json::value::RawValue, if that lands (JSON RawValue support #506)

All are part of the json module, originally added in #483, which is still under ## [Unreleased] in the changelog. No released version of maud is affected, so this can be fixed without a compatibility break.

Root cause

Maud has no notion of attribute context. Splices in attribute values and in element content go through the same code path in maud_macros/src/generate.rs. Escaping is therefore entirely the responsibility of each Render impl. Render for str escapes via Escaper, so it is safe in both positions. The JSON impls deliberately write raw bytes so that < escapes survive, which makes them safe in element content but not in an attribute.

Why this can't be fixed in the JSON formatter

The problematic quotes are the JSON string delimiters, emitted by serde_json's Formatter::begin_string / end_string. They cannot be escaped as \u0022 without producing invalid JSON – a JSON string must begin with a literal ". Quotes inside a string are already escaped to \" by serde_json before the custom formatter sees the fragment, and \ carries no meaning to an HTML attribute parser, so that does not help either.

Any JSON document containing a string will always emit ".

How might we fix this

  1. Document the restriction. JSON RawValue support #506 does this already. It's cheap but it leaves a sharp edge.
  2. Escape at the attribute layer. Have generate.rs wrap attribute-value splices in an escaping adapter. This would change behaviour for every Render impl in attribute position, including PreEscaped.
  3. Distinguish the two contexts in the trait. For example a separate method for attribute rendering, defaulting to render_to, that the JSON impls override to escape quotes.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions