Showing posts with label OSS. Show all posts
Showing posts with label OSS. Show all posts

Sunday, May 17, 2020

CVE-2020-11022/CVE-2020-11023: jQuery 3.5.0 Security Fix details

jQuery 3.5.0 was released last month. In this version, two bugs which I reported are included as "Security Fix".

jQuery 3.5.0 Released! | Official jQuery Blog
https://blog.jquery.com/2020/04/10/jquery-3-5-0-released/

The bugs are regsited as CVE-2020-11022 and CVE-2020-11023:

https://github.com/advisories/GHSA-gxr4-xjj5-5px2
https://github.com/advisories/GHSA-jpcq-cgw6-v4j6

 In this article, I'd like to explain the bugs' details.

Overview of Problems


The application which has the following features is affected:

  • The application allows users to write any HTML (but it is sanitized)
  • The application dynamically appends the sanitized HTML with jQuery

The following code is an example of a such application:
<div id="div"></div>
<script>
//Sanitized safe HTML
sanitizedHTML = '<p title="foo">bar</p>';
//Just append the sanitized HTML to <div>
$('#div').html(sanitizedHTML);
</script>
In this situation, if the sanitization is performed properly, it looks like that usually XSS does not occur since it just appends the sanitized safe HTML. However, actually, .html() does the special string processing internally and it caused XSS. This is the issue which I'll explain in this article.

PoCs


There are many variations but I'll show basic three PoCs. Usually, JavaScript is not executed from the following HTMLs:

PoC 1.
<style><style /><img src=x onerror=alert(1)> 
PoC 2. (Only jQuery 3.x affected)
<img alt="<x" title="/><img src=x onerror=alert(1)>">
PoC 3.
<option><style></option></select><img src=x onerror=alert(1)></style>
You might think there is an img tag which has an onerror attribute, but if you check carefully, actually, it is placed in the attribute or inside of the style element, you will notice that it is not executed. So, even if those HTMLs are generated by an HTML sanitizer as the sanitized HTML, it is not unnatural at all.

However, in all cases, if the HTML is appended via jQuery .html(), JavaScript is executed unexpectedlly.

You can test each PoC in:
https://vulnerabledoma.in/jquery_htmlPrefilter_xss.html

Next, I'll explain why it happens.

CVE-2020-11023: Root cause (PoC 1,2)


The PoC 1 and 2 have the same root cause. Within the .html(), the HTML string passed as the argument is passed to the $.htmlPrefilter() method. The htmlPrefilter performs the processing for replacing the self-closing tags like <tagname /> to <tagname ></tagname>, by using the following regex:

rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi
[...]
htmlPrefilter: function( html ) {
  return html.replace( rxhtmlTag, "<$1></$2>" );
If the PoC 1's HTML passes through this replacement, the output will be:
> $.htmlPrefilter('<style><style /><img src=x onerror=alert(1)>')
< "<style><style ></style><img src=x onerror=alert(1)>"
The yellow part is the replaced string. Due to this replacement, the <style /> inside the style element is replaced to <style ></style> and as the result, the string after that is kicked out from the style element. After that, the .html() assigns the replaced HTML to innerHTML. Here, the <img ...> string becomes an actual img tag and it fires the onerror event.

By the way, the above regex is used in jQuery before 3.x. Since 3.x, another regex which is a bit modified is used:

https://github.com/jquery/jquery/commit/fb9472c7fbf9979f48ef49aff76903ac130d0959#diff-169760a97de5c86a886842060321d2c8L30-R30
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi

This change introduced another XSS vector which can cause XSS by more basic elements and attributes only. The PoC 2's vector is introduced by this change. It works on jQuery 3.x only.
> $.htmlPrefilter('<img alt="<x" title="/><img src=x onerror=alert(1)>">')
< "<img alt="<x" title="></x"><img src=x onerror=alert(1)>">"
In this case, the <img ...> string on the attribute's value is kicked out and XSS happens.

I explained the root cause of PoC 1 and 2. How did the jQuery team fix this?

The Fix (PoC 1,2)


jQuery Team fixed this issue by replacing the $.htmlPrefilter() method to an identity function. Therefore, the passed HTML string is no longer modified by the htmlPrefilter function now.

https://github.com/jquery/jquery/commit/90fed4b453a5becdb7f173d9e3c1492390a1441f#diff-169760a97de5c86a886842060321d2c8L201-R198

However, this did not solve all XSS issues. Inside the .html(), another string processing is performed and introduces another problem (PoC 3).

CVE-2020-11022: Root cause (PoC 3)


Inside the .html(), if the tag that appears at the beginning of the HTML which is passed as an argument is one of specific tags, jQuery tries to wrap it with another tag once and do the next processing. This is because some tags are automatically removed due to the HTML's specification or browser's bug if there is no wrapping processing.

The opiton element is one of such elements - in MSIE9, due to its bug, the option element is automatically removed when it is assigned to innerHTML, if it is not wrapped with the select element.

To deal with this, jQuery tries to wrap the entire passed HTML string including that element with the <select multiple='multiple'> and </select> if the passed HTML string's first element is the option element.

The wrapped tags are defined in:
https://github.com/jquery/jquery/blob/3.4.1/src/manipulation/wrapMap.js#L9

The actual wrapping processing is done in:
https://github.com/jquery/jquery/blob/d0ce00cdfa680f1f0c38460bc51ea14079ae8b07/src/manipulation/buildFragment.js#L39

The issue of PoC 3 happens via this wrapping processing. If the PoC 3's HTML passes through this wrapping processing, the HTML will be:
<select multiple='multiple'><option><style></option></select><img src=x onerror=alert(1)></style></select>
When this HTML is assigned to innerHTML in the jQuery's internal code, JavaScript is executed.

The reason why the script is executed is in the <select> tag's parsing.The <select> does not allow putting HTML tags except the option, optgroup, script and template element inside that element. Due to this specification, the inserted <style> is just ignored, the </select> inside the <style> becomes an actual select element's closing-tag, and then <select> block is closed there. Eventually, the next <img ...> is kicked out from the <style> and the onerror event fires -> XSS. This was the root cause.

The Fix (PoC 3)


jQuery Team fixed this issue by applying the wrapping procesing to MSIE9 only.

https://github.com/jquery/jquery/commit/966a70909019aa09632c87c0002c522fa4a1e30e#diff-51ec14165275b403bb33f28ce761cdedR25

MSIE9 is not vulnerable to this issue because MSIE9's <select> parsing is a bit special (yes, it's wrong). Therefore, applying the wrapping processing to MSIE9 only can solve this problem.

For your information, these issues exist not only in .html(), but also in .append(), $('<tag>') etc. Basically, the issue happens via the APIs in which the $.htmlPrefilter() method or wrapping processing is used internally.

Update it


If your application is appending the sanitized HTML via the jQuery functions, you should update to 3.5.0 or higher. If an updating is hard in some reason, I recommend sanitizing the HTML by using DOMPurify, which is XSS sanitizer. DOMPurify has a SAFE_FOR_JQUERY option and it can sanitize with considering the jQuery's behavior. You can use that, like this:

<div id="div"></div>
<script>
unsafeHtml = '<img alt="<x" title="/><img src=x onerror=alert(1)>">';
var sanitizedHtml = DOMPurify.sanitize( unsafeHtml, { SAFE_FOR_JQUERY: true } );
$('#div').html( sanitizedHtml );
</script>
Note that DOMPurify had the bypass in SAFE_FOR_JQUERY recently. Please make sure that you use 2.0.8 or higher.

In the end


I started to investigate this issue from XSS challenge by @PwnFunction:
https://xss.pwnfunction.com/challenges/ww3/

Actually, the some of these bugs were known and it was the expected solution of this challenge. (You can find that fact in the DOMPurify's change log. It was already known in 2014 at least and DOMPurify has the SAFE_FOR_JQUERY option since 2014. )

With the challenge as a trigger, I started to read jQuery's source code again and I noticed another vector (PoC 2), which is not mentioned publicly. Since this vector can allow XSS with the easy elements and attributes only, I thought many applications are vulnerable. When I actually investigated it, I found some vulnerable apps immediately. I reported it to the developer of the affected applications, at the same time I thought that this issue should be fixed by jQuery side, so I decided to report this to jQuery team. The jQuery team were quick to address the issues, even though they had to make breaking changes. Thank you jQuery team. Also, thanks to @PwnFunction, the creator of the XSS challenge, who gave me an opportunity to investigate this issue.

That's it. I hope this article helps for securing your web application or finding bugs.

Sunday, April 10, 2016

Abusing docmode inheritance: EasyXDM 2.4.19 DOMXSS

In this post, I would like to explain an XSS issue of EasyXDM 2.4.19.
I reported it and it was fixed by the developer.

If you are users, you should update it to EasyXDM 2.4.20.

Release Security update - 2.4.20 · oyvindkinsey/easyXDM · GitHub
https://github.com/oyvindkinsey/easyXDM/releases/tag/2.4.20


This bug is different from the following @kkotowicz's bugs.

http://blog.kotowicz.net/2013/09/exploiting-easyxdm-part-1-not-usual.html
http://blog.kotowicz.net/2013/10/exploiting-easyxdm-part-2-considered.html
http://blog.kotowicz.net/2014/01/xssing-with-shakespeare-name-calling.html

Technical Details

To reproduce this bug, we need a little unusual trick. This bug works on only MSIE. Also, the document mode should be old (IE7 or 5). This is because XSS exists in the part where only buggy browser can pass through. And only old document mode can meet this condition.

Let's look at the code. XSS occurs in the following createElement() part:

https://github.com/oyvindkinsey/easyXDM/blob/2.4.19/src/Core.js#L507-509
    if (HAS_NAME_PROPERTY_BUG) {
        frame = document.createElement("<iframe name=\"" + config.props.name + "\"/>");
    }

The HAS_NAME_PROPERTY_BUG variable of the if statement condition becomes true on an only buggy browser. It is assigned in the following function:

https://github.com/oyvindkinsey/easyXDM/blob/2.4.19/src/Core.js#L474-482
function testForNamePropertyBug(){
    var form = document.body.appendChild(document.createElement("form")), input = form.appendChild(document.createElement("input"));
    input.name = IFRAME_PREFIX + "TEST" + channelId; // append channelId in order to avoid caching issues
    HAS_NAME_PROPERTY_BUG = input !== form.elements[input.name];
    document.body.removeChild(form);
    // #ifdef debug
    _trace("HAS_NAME_PROPERTY_BUG: " + HAS_NAME_PROPERTY_BUG);
    // #endif
}
This code creates a form element and a input element with a name attribute and tries to access to a input element via the name. It seems that IE7 mode cannot access to a element created dynamically with JavaScript via the name. And this code is for testing it.

I created a page for testing this part. You can confirm that the HAS_NAME_PROPERTY_BUG variable becomes true from MSIE.

http://vulnerabledoma.in/easyxdm/name_property_test.html

So, let's confirm this XSS using the vulnerable EasyXDM.

Go to the following URL and change IE7 mode via F12 Developer Tools( F12 -> Emulation tab ). You should see an alert dialog.

http://vulnerabledoma.in/easyxdm/2.4.19_index.html?xdm_e=http%3A%2F%2Fvulnerabledoma.in&xdm_c=%22onload%3dalert(document.domain)//&xdm_p=0

OK, we knew that it actually works. But it works on only IE7 mode so far. This means that an opportunity of attack is very limited.

So, we use the trick called "document mode inheritance" which I took up in my slides recently.



Let's use this!

http://l0.cm/easyxdm/poc.html
<meta http-equiv="x-ua-compatible" content="IE=5">
<iframe src="//vulnerabledoma.in/easyxdm/2.4.19_index.html?xdm_e=http%3A%2F%2Fvulnerabledoma.in&xdm_c=%22onload%3dalert(document.domain)//&xdm_p=0"></iframe>
<script>document.write("document.documentMode: "+document.documentMode)</script>
You still cannot see an alert dialog. The parent window is running in IE5 mode by the meta tag. But the iframe is running in IE8 mode. This is because the iframe page has <!DOCTYPE html>. If it exists, IE11 can bring down the document mode until IE8 mode. To XSS, we somehow have to bring down to IE7 mode.

No worries! Recently I found that IE has the strong inheritance ability at the specific place. You can test this from:

http://l0.cm/easyxdm/poc.eml

Finally, you can see an alert dialog. A difference between the former and this page is that the page is a Content-Type: message/rfc822 format, not text/html. As I have shown in the following my slides, IE/Edge still can open a message/rfc822 document into the browser.


It seems that a message/rfc822 document is rendered in IE5 mode by default. And it seems its docmode inheritance ability is stronger more than a text/html page. Even if the page has <!DCOTYPE html>, we can bring down the document mode until IE7 mode. Due to this, we can reach the vulnerable code. w00t! :)

Using this technique, it can extend the impact. The problem affecting only IE7 mode becomes the problem affecting all IE11 users without being changed to an old document mode by users. It's very useful technique, isn't it? :)

I wrote about XSS in EasyXDM 2.4.19.
That's all. Thank you for reading my post!