I noticed that when table header IDs are enabled, the raw header text
gets dropped straight into an id="" attribute with almost no escaping.
In src/subParsers/makehtml/tables.js, the parseHeaders function does
this:
id = ' id="' + header.replace(/ /g, '_').toLowerCase() + '"';
That's it. Spaces become underscores, everything gets lowercased, and
then it's concatenated into the attribute. No HTML encoding at all.
Double quotes, angle brackets, everything passes through.
So if the table header contains a " character, you break out of the
id attribute. And since < and > pass through too, you can inject any
HTML you want after the breakout.
This matters because tablesHeaderId is enabled by default in the
"github" flavor, which is what most people use:
converter.setFlavor('github'); // sets tablesHeaderId: true
PoC — put this markdown through showdown with github flavor:
| "><svg/onload=alert(1)> | Col2 |
Showdown produces:
<th id=""><svg/onload=alert(1)>">...
The browser sees:
-
— th with empty id
- <svg/onload=alert(1)> — SVG executes the JS
The /onload trick works because HTML5 parsers treat / after a tag name
as a transition to the "self-closing start tag" state, which on a
non-void character falls back to "before attribute name" — so
onload=alert(1) becomes a valid attribute.
You don't need spaces at all in the payload, which is convenient since
spaces get replaced with underscores.
I noticed that when table header IDs are enabled, the raw header text
gets dropped straight into an id="" attribute with almost no escaping.
In src/subParsers/makehtml/tables.js, the parseHeaders function does
this:
That's it. Spaces become underscores, everything gets lowercased, and
then it's concatenated into the attribute. No HTML encoding at all.
Double quotes, angle brackets, everything passes through.
So if the table header contains a " character, you break out of the
id attribute. And since < and > pass through too, you can inject any
HTML you want after the breakout.
This matters because tablesHeaderId is enabled by default in the
"github" flavor, which is what most people use:
PoC — put this markdown through showdown with github flavor:
Showdown produces:
The browser sees:
The /onload trick works because HTML5 parsers treat / after a tag name
as a transition to the "self-closing start tag" state, which on a
non-void character falls back to "before attribute name" — so
onload=alert(1) becomes a valid attribute.
You don't need spaces at all in the payload, which is convenient since
spaces get replaced with underscores.