Steps to reproduce:
- Create a page named PageThatExists
- On some other page (not PageThatExists), create an edit with the following edit summary: [[Special:RecentChanges#%1b0000000|link1]] [[PageThatExists#/autofocus/onfocus=alert("xss\n"+document.domain)//|link2]]
- View the history of that page. [This assumes that the edit you just made was the most recent edit to the page]
- An alert box pops up announcing the XSS.
Suggested fix:
- Change marker syntax in MediaWiki\CommentFormatter\CommentParser to have " and ' in it like Parser does instead of being just \x1b
Explanation:
This is similar to T110143
During parsing, page titles get url decoded (It should be noted, that this doesn't work in all spots, because some things decode \x1b as the unicode replacement character, particularly entity decoders). This allows the attacker to insert an \x1B character after they were all stripped, and hence allows the attacker to insert a link marker. If a link marker is inserted inside an attribute.
Whether the link is directly inserted or if it uses a strip marker depends on if the link is in link cache. Additionally non existing links generally don't have a fragment (The actual page name is not allowed to have 0x1B in it). So we want the first link to always be in link cache, and the second link to not be in link cache but be an existing page. It should be noted, that this cache, as well as the marker numbering, is shared across all the edit summaries on the page (including one more than is shown, so limit=1 would actually have 2 edit summaries parsed). Special pages always exist so they are treated as if they are always in link cache regardless of what is there. Then we just have to hope that the other page we use is not, but as long as it isn't mentioned anywhere else on the page, we should be good.
The end result is - The special recentchanges link is substituted immediately. It has a marker. When markers are replaced there is one in the attribute that is replaced, this breaks out of the html.
So after first step we get: (pretend \x1B are a literal U+001B character)
<a href="/w/index.php/Special:RecentChanges#\x1B000000" title="Special:RecentChanges">link1</a> \x1B0000000
Then we do finalize to replace link markers which gives us:
<a href="/w/index.php/Special:RecentChanges#<a href="/w/index.php/Test#/autofocus/onfocus=alert("xss\n"+document.domain)//" title="Test">link2</a>" title="Special:RecentChanges">link1</a> <a href="/w/index.php/Test#/autofocus/onfocus=alert("xss\n"+document.domain)//" title="Test">link2</a>
HTML parses that as if it is:
<a href="/w/index.php/Special:RecentChanges#<a href=" w="" index.php="" Test#="" autofocus onfocus="alert("xss\n"+document.domain)//"" title="Test">link2</a>" title="Special:RecentChanges">link1</a> <a href="/w/index.php/Test#/autofocus/onfocus=alert("xss\n"+document.domain)//" title="Test">link2</a>
Browser sees autofocus attribute so focuses the link on page load. Then the onfocus event is triggered, so the JS runs.