DOMPurify bypass: XSS via HTML namespace confusion
research.securitum.com
research.securitum.com
I know why I don't like automatically closing elements. Xhtml for the win!
<p>List: <ul><li>item1</li></ul> of items</p>
becomes <p>List: </p><ul><li>item1</li></ul> of items<p></p>
and that's probably not what you wanted. So you have to understand auto-closing behaviour either way. And if you understand it, you can just spare yourself from closing them.Now with XHTML, that's a different thing. But I think that ship sailed many years ago and HTML is a preferred way to go nowadays.
> The pc tag identifies a paragraph continuation -- that is, one or more sentences related by their subject matter to a paragraph which has been interrupted by an address, example, figure, list, or long quotation. > Usage: The paragraph continuation can occur after the sequence consisting of a paragraph unit followed by an address, example, figure, list, or long quotation.
:p.The subject of a paragraph might be continued through
:sl
:li.an address, a list,
:li.an example or figure, or
:li.a long quotation, :esl
:pc.and continue to be discussed in flowing text.
The discussion could continue indefinitely through
To long to quote entirely [3], unfortunately IBM destroyed online version[1] https://www.google.com/search?q=list+inside+paragraph+site%3...
[2] SH20-9160-0 DCF Generalized Markup Language GML Users Guide, First Edition (July 1978)
Actually that is not really true. It is defined per html5 spec what is closed automatically when another open node is being parsed.
The <p> element always had a weird flow-root behaviour, that is why it is always closed automatically.
Rather than that iirc mostly form relevant elements are also closed automatically, like optgroup, option, select, input and such.
Additionally it's only table (same problem with flow root) and body, pretty much.
I can understand that a lot of people are confused why that is. But the reason is not the difference of XML vs SGML per se (xhtml will simply break if a p is within a p)... it's the flow root model and the difference in behaviours of layouting that is specified here, not the notation structure.
So SGML was the better choice, to allow all php crapsites that never test their html validity to still run rather than forcing the enduser to (fix?) the xml.
I mean, at some point opera didn't even work on bbcode based forum software(s), so people quickly started abandoning it.
I'm currently building my own HTML5 parser for my browser stealth [1] and I aim to be spec-compliant with it, and this might come in very handy for testing against.
What I kind of miss with SGML as a feature is something similar to XSLT stylesheets that can transform chunks of a website into another chunk.
Currently I'm kind of reinventing the wheel here due to my optimizer having the idea to "upgrade" websites on the fly before they get to the client.
If all websites were XHTML1.1 strict based, that part would have been so much easier.
<div bla=x>
in your main doc, you can make SGML expand it using <!DOCTYPE div SYSTEM [
<!ENTITY bla SYSTEM>
]>
<div>
<p>Value of bla is &bla</p>
</div>
honoring escaping/sanitizing etc. LPDs can apply rules in a context-dependent way using an automaton capturing much of core CSS.Now, for arbitrary markup manipulation (XSLT is Turing-complete), don't tell the HN crowd that SGML has/had Scheme-based DSSSL (precursor of XSLT) ;) My opinion, having done large, nontrivial XSLT projects (including extracting the DTD grammar rules you see on the site from spec text) is that the more complex it gets, the more a general-purpose language with unit testing etc becomes a better choice over XSLT.
Edit: much luck with your browser project! Don't hesitate to use my code or ask questions (here or on StackOverflow tagged sgml)
It's as in JavaScript: you can omit the ";" at the end of a statement and the parser will figure it out, but it only makes the parser more complex and introduces subtle differences you have to learn. If JS parser were more strict, it would be simpler both internally and conceptually.
MDN definition of phrasing content: https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Cont...
You do, if you use HTML, but my read of the above two comments was that they would've preferred if the world had stuck on the XHTML path.
> Now with XHTML, that's a different thing. But I think that ship sailed many years ago and HTML is a preferred way to go nowadays.
Actually, XHTML is a (little-known) part of the HTML5 spec.[0], so going the strict path is still an option. In the past, this would've required complex content-negotiation for media-type backward-compat but that's no longer an issue unless a non-neglible % of your visitors are using IE8.
The only remaining issue is that of draconian error handling, which is an issue browsers definitely would have fixed the UX of had the mainstream stayed on the XHTML track, but sadly that never happened. Still, good modern support for server-side validation of well-formed XML documents means this is also less of an issue than it once was (though tbh, still a significant issue imo).
W3 have also put together a more informal guide to modern XHTML considerations within the HTML5 spec. https://dev.w3.org/html5/html-polyglot/html-polyglot.html
Back in the dim and distant past when XHTML was a fashionable term to throw around, Evan Goer did a study of whether sites claiming to serve XHTML were actually doing so. The results were not pretty [1]. Some people took the results as a challenge and tried to ensure they were sending valid XHTML with the correct mime type so that browsers would catch fire in the case of a parsing failure. In almost every case it turned out to be possible to get their sites to break with user generated content (e.g. searchng for XML-invalid characters which were then echoed back onto the page).
So I contend we ran the XHTML experiment pretty thoroughly 15 years ago, and it turns out that it doesn't really work. Once you accept that parsing errors being fatal isn't viable for publishing, you have to have some kind of error recovery system. The one in HTML isn't ideal since it's basically just the codification of many years of improvisation and reverse engineering. Maybe something like XML5 [2] would be better. But figuring out how to move the world in that direction is an unsolved problem. Meanwhile HTML Just Works for most of the people most of the time.
[1] https://goer.org/Journal/2003/04/the_xhtml_100.html [2] https://github.com/annevk/xml5
The industry didn't defeat SQL injection through more permissive query parsers, but by generating queries via ORMs and parametrised templates instead of dumb string concatenation.
Also, have you noticed that JSON injection bugs are almost never heard of? That's because in many programming languages this very problem comes pretty much pre-solved before you even add any JSON support to them.
Imagine Word or Excel document that's trying to silently recover. I do not see programming languages adopting "not to fail" approach
1 + "2"
//"12"
1 - "2"
-1
There were PHP sites with mysql connection error all around. As industry we've chosen AirBrake approach — fail and notify developers. HTML makes it easier to edit plain text but there is a price. What you load is not what you've stored, HTML is a lossy serialization [1] [2] [3].Program not human produced DOM, it should be safe to serialize-deserialize. It could be JSON, XML, s-expressions. It is unsafe with HTML.
It is very easy to author XHTML. Start DOM first (HTML for brevity):
data:text/html;charset=UTF-8,<p contenteditable>foo
Done, it automatically escapes <>&. Extend with some controls [4], store it as XHTML [5]. It is WYSIWYG, much easier than HTML authoring.[1] http://sergeykish.com/script-style-is-cdata-in-html
[2] http://sergeykish.com/content-after-html-appended-to-body-in...
[3] http://sergeykish.com/pre-newline-ignored-in-html-test
Typically what breaks is styling. It might not be pretty, but it may still be functional.
I may have <ul> in <p> (we had it in 1978), I may have <a> in <script> (and it works like comment), I may have <pre>\n and don't worry that it disappear each time I save document. I may have nested <script type="foo"> tags [1].
DOM supports it. XHTML supports it. HTML breaks my content on save-load.
I wrote a Semgrep rule (I'm one of the maintainers) to look for calls to the .sanitize() method and suggest the final recommendation in the post, which is to use either RETURN_DOM or RETURN_DOM_FRAGMENT. To use it:
pip install semgrep && semgrep --config=https://semgrep.dev/s/ievans:dom-purify2
Or you can just preview it on some test code: https://semgrep.dev/s/ievans:dom-purify2The simplest and safest solution for sites would be to implement Content-Security-Policy with a good script-src (with limited sources and without the 'unsafe-inline' escape hatch).
Unfortunately, even HN itself uses a script-src with unsafe-inline, and it allows everything on cloudfare's CDN...
iirc, IE11 (under Windows 7, specifically) does not support CSP. I don't think CSP mitigates all XSS vectors either (`<a href="javascript:alert(1)">` for example). Sure IE11 is deprecated but that doesn't mean you don't need to account for it when building an enterprise application.
I'm curious if you can provide any details on what / how Safari was exploitable with CSP - https://caniuse.com/?search=content-security-policy indicates that it should be pretty uniform across popular browsers. If you'd prefer a private channel @yoloClin on twitter.
I agree that one should still sanitize input, at least for fields which allow HTML* , but it's obvious XSS filtering/sanitization can introduce XSS as much as not. This article is merely one example, there were enough to make Chrome give up and turn off their XSS filter. So main defence should be CSP and sanitization is just a nice-to-have.
* Because sanitized input is often saner than the nonsense users can insert when they are allowed to put in tags. Basically use sanitization as an HTML Tidy with extra filtering. Also for very old browsers.
My case was about the allow-scripts directive of the sandboxed iframe, which I thought was linked to the csp mechanism, but now that I checked the documentation, it seems that I was wrong.
I basically display a random HTML document in an sandboxed iframe with disabled scripts. When you do so on Chrome and Firefox, the event listeners injected inside the iframe from a script in the parent frame still works, but on Safari it does not because all the scripts (or events) inside the iframe are disabled.
So rather that relying on this mechanism, I used DOMPurify to filter all the scripts.
Although, I guess, now that I think about it, the fact that you are making modifications means I guess there could still be attacks that rely on mutations that potentially happen after those modifications...
[1] https://github.com/angular/angular/blob/2038568f3e631cb15e91...
---------
Thus, going back to the snippet:
<form id="outer"><div></form><form id="inner"><input>
In the beginning, the form element pointer is set to the one with id="outer". Then, a div is being started, and the </form> end tag set the form element pointer to null. Because it’s null, the next form with id="inner" can be created; and because we’re currently within div, we effectively have a form nested in form.---------
I was under the impression that a closing tag automatically also closed any tags that were currently open, but contained within the tag being closed. Is it not a bug that, when the </form> tag is encountered, the open <div> tag isn't closed at that point?
<form><math><mtext><form><mglyph><style></math><img src onerror=alert(1)></style></mglyph></form></mtext></math></form>
This seems to be the wrong part. If "</math><img src onerror=alert(1)>" was interpreted as part of a #text node, it should have been serialized into "</math><img src onerror=alert(1)>" so that it becomes harmless in any subsequent round-trip.Even PHP provided a function to do that 20 years ago. It should be relatively straightforward in any modern language with regexp support.
But this isn't true at all. You could have text that's supposed to look like "<span>" -- just look at the discussion we're having right now. And that text needs to be rendered as "&lt;span&gt;", but it also needs not to be rendered as "&amp;lt;span&amp;gt;". You have to keep track of what you've done to it.
You will not be able to do this if your approach is "doesn't matter; we'll just trust whatever we get".
Sanitizing HTML isn't just about removing unwanted tags and attributes. Assuming you want to preserve some styling, the sanitizer needs to be aware of the spec for not only HTML but also CSS, JS, SVG, and whatnot. It must ensure that it never emits output that would be invalid in any of those contexts. (JS is easy because you normally just delete it.)
To date, I haven't seen any HTML sanitizer that's as serious about enforcing spec as HTMLPurifier. It strips out everything that isn't in a whitelist and guarantees that the output is syntactically valid. Unfortunately, it's written in PHP, its whitelist is kinda outdated, and nobody seems to want to port it to any other language. But the only alternative is to take dangerous shortcuts.
* { font-family: '</math>...'; }
Fastmail uses DOMPurify for this purpose, augmenting it with rewriting the selectors in style blocks in order to scope them safely.
(Fastmail also sponsors the DOMPurify bug bounty—I presume the latest mXSS listed at https://www.fastmail.com/about/bugbounty/ corresponds to this thing, and as noted, this issue didn’t affect Fastmail; the reason for that is that Fastmail uses the DOM that DOMPurify returns, rather than problematic and less efficient serialising and deserialising approach.)
Remember with this that each application can choose what it wants to allow or deny. It would be very harmful for DOMPurify to not allow certain sorts of tags just because you can’t immediately think up a reason why you might want to use it.
sample = `<form><math><mtext></form><form><mglyph><style></math><img src onerror=alert(1)>`
fragment = DOMPurify.sanitize(sample, {RETURN_DOM_FRAGMENT: true})
body = (new XMLSerializer).serializeToString(fragment)
//...<style></math><img src onerror=alert(1)></style>...
div.innerHTML = body
div.append(fragment)
iframe.srcdoc = body
It renders nice </math><img src onerror=alert(1)>It's effectively a manual process of calling appropriate escaping (html, css, js) at every dynamic output site. Ensuring you are doing it 100% in thousands if not millions of calls is daunting.
I have seen security tools crawling your app and using payload to test for vulnerability which sounds nice in theory but in a really complex application where you need to understand the business rules to even crawl the whole application (not get caught up on validation/error) how do people ensure that?
Instead, use e.g. an HTML library that automatically escapes strings. Then you don't even have to think about it.
I have a feeling it will remain a very manual and diligent process. Be always on top of new techniques and solutions, have a good understanding how everything works in detail and reduce your attack surface by keeping things simple.
If you're always using safe apis, its really easy to verify that you dont do the wrong thing (e.g. via static analysis, or even grep. Browsers are even now suporting trusted types to dynamically verify this at runtime)
The result? The website of my little diablo 2 clan was the only one in my group of friends that did not get hacked. Due to me using an obscure library that did the right thing in a language nobody liked. It wasn't any victory of mine. Had I used PHP I would have been just as pwned as my friends.
This is a myth, the issue is if you learn by Google instead of using something like a book. All languages and libraries (that I used) allow you to run raw sql if you need and all books/good learning materials when introducing SQL will explain how to do the correct thing with escaping parameters or even nicer using bound paramters.
That was the knowledge offered for us 13 years olds wanting to write a discussion board for our game groups. I think it is a lovely example of "the simple thing should be the safe thing, and if you need more there should be ample warning". That was not the case of PHP 4.0.
Anecdote: the Swedish PHP book (I recall there being only one) that most of us got taught the string building approach without any escaping. The oldest things I can find Googling now are from 2007 and are almost all safe, either through PDO or a safe mysqli interface.
The raw queries were hidden in a sqlite3/DANGEROUS library. Despite doing things like stepping queries, bypassing the statement cache or mucking around and changing parameterized queries I didn't have to touch the DANGEROUS API.
The trouble comes is in an existing code base if you know 95% of those use are valid but to pass the check you know have to migrate that all and do QA.
It depends a lot on the library I guess, does it make it easier to do the right thing or not.
In HTML generation, that means you never dynamically parse HTML generated directly from raw data. E.g. in React, you define your content entirely in terms of components that are explicitly defined in your code (no HTML parsing - they're objects in your code, explicitly built by your code) and content for those components (still no HTML parsing, they directly become text either for attribute params, or for text nodes that sit within your HTML tags).
It's the same as protecting against SQL injection. Don't interpolate user input directly into a SQL string and then parse it. Define/generate the query in your code separately, with explicit parameter placeholders for where dynamic input will go, and then provide variables that can be safely used for those parameters, but which are never parsed as part of the query itself.
For HTML, the only time this becomes challenging is when you accept user input that is itself HTML already. In that case, you have to parse it, and you have a big problem.
Don't do that, wherever possible. Accept structured data, accept plaintext text, accept markdown (without HTML tags either disabled or _extremely_ carefully limited), but wherever possible, don't accept and output HTML given to you by a user. For similar reasons, don't accept SQL queries uploaded by a user.
Dompurify exists for the case where you can't do this, but in general it's better, and not that hard, to avoid needing it at all.
It seemed to have no issues whatsoever (click Run): https://play.golang.org/p/oBYXo9MDusr https://play.golang.org/p/KzD1Ug-KKiB
<math><mtext><table><mglyph><style><math><table id="</table>"><img src onerror=alert(1)>
Works on Firefox, fail to reproduce bypass on Chrome http://sergeykish.com/dompurify-bypassI've asked author, looks like Chrome parses src and srcdoc differently, works with innerHTML.
It work in Chrome on
div.innerHTML = sanitized
but not on iframe.srcdoc = sanitized
iframe.src = `data:text/html;charset=UTF-8,${sanitized}`
I am rewriting document so it is possible to check different methods.https://github.com/wikimedia/VisualEditor/blob/master/src/ve...
The error here is to use a blacklist instead of a whitelist.
The fact that their list is not namespace-specific is a bigger issue imo.