Bug 66585 - XSS filter bypass via document.write(location.href) and fragments
Summary: XSS filter bypass via document.write(location.href) and fragments
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: WebKit Misc. (show other bugs)
Version: 528+ (Nightly build)
Hardware: All All
: P2 Normal
Assignee: Daniel Bates
URL:
Keywords: XSSAuditor
Depends on:
Blocks: 66579
  Show dependency treegraph
 
Reported: 2011-08-19 14:01 PDT by Adam Barth
Modified: 2011-08-31 13:51 PDT (History)
4 users (show)

See Also:


Attachments
LayoutTest (1.99 KB, patch)
2011-08-24 14:48 PDT, Daniel Bates
no flags Details | Formatted Diff | Diff
DRT Test Case (1.23 KB, patch)
2011-08-24 17:09 PDT, Daniel Bates
no flags Details | Formatted Diff | Diff
Revised test case showing the issue. (1.91 KB, patch)
2011-08-29 14:15 PDT, Thomas Sepez
no flags Details | Formatted Diff | Diff
Patch plus test case changes from prev attachment. (7.13 KB, patch)
2011-08-31 11:52 PDT, Thomas Sepez
abarth: review-
Details | Formatted Diff | Diff
Patch from prev attachment with corrected indent style. (7.07 KB, patch)
2011-08-31 12:22 PDT, Thomas Sepez
abarth: review+
Details | Formatted Diff | Diff
Prev patch plus renamed length variable. (7.10 KB, patch)
2011-08-31 12:33 PDT, Thomas Sepez
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Adam Barth 2011-08-19 14:01:33 PDT
http://code.google.com/p/chromium/issues/detail?id=76796

VULNERABILITY DETAILS
There is a way to bypass anti-XSS filter for DOM XSS exploiting a "window.location.href".

Considering a typical URL:

scheme://domain:port/path?query_string#fragment_id

Browsers encode correctly both "path" and "query_string", but not the "fragment_id". 
Explorer does not encode neither "query_string".
So if used "fragment_id" the vector is also not logged on Web Server.

VERSION
Chrome Version: 10.0.648.134 (Official Build 77917) beta

REPRODUCTION CASE
This is an xss_location.html page:

[[
<script type="text/javascript" language="javascript"> 
	document.write( window.location.href );
</script>
]]

The attack vector is:
xss_location.html?#<script>alert('XSS');</script>

* PoC:
For your convenience, a minimalist PoC is located on:
http://security.onofri.org/xss_location.html?#<script>alert('XSS');</script>

* References
 - DOM Based Cross Site Scripting or XSS of the Third Kind - http://www.webappsec.org/projects/articles/071105.shtml
Comment 1 Daniel Bates 2011-08-24 11:43:02 PDT
I am unable to reproduce this issue using either Safari- or Chrome-  for Mac,  Version 5.1 (7534.48.3, r93670) and 14.0.835.109 beta, respectively.

Are there any additional details?

I did notice that Chrome doesn't visibly encode the fragment portion of the URL which is consistent with both the description of this bug as well as reiterated in <http://code.google.com/p/chromium/issues/detail?id=76796#c7>. When running the example in Chrome, we attempt to construct the HTML Script Element. The XSS Auditor detects this and scrubs the contents of the <script>.

In comparison, in Safari the fragment portion of the URL is encoded and hence we don't interpret as HTML markup.

For completeness, we have some test coverage for DOM-based XSS via window.location. For example, <http://trac.webkit.org/browser/trunk/LayoutTests/http/tests/security/xssAuditor/dom-write-location.html>.
Comment 2 Adam Barth 2011-08-24 13:51:32 PDT
I also can't reproduce.  Maybe we should add a test a test and close this issue?  I tried this variation as well:

http://security.onofri.org/xss_location.html?#<script>alert('XS%41S');</script>
Comment 3 Daniel Bates 2011-08-24 14:19:06 PDT
(In reply to comment #2)
> I also can't reproduce.  Maybe we should add a test a test and close this issue?

Actually, we already have a test case that covers this issue:
<http://trac.webkit.org/browser/trunk/LayoutTests/http/tests/security/xssAuditor/anchor-url-dom-write-location.html>.

>  I tried this variation as well:
> 
> http://security.onofri.org/xss_location.html?#<script>alert('XS%41S');</script>

I'll add a test for this variation.
Comment 4 Adam Barth 2011-08-24 14:31:14 PDT
Thanks Dan.
Comment 5 Daniel Bates 2011-08-24 14:48:34 PDT
Created attachment 105073 [details]
LayoutTest
Comment 6 WebKit Review Bot 2011-08-24 15:36:07 PDT
Comment on attachment 105073 [details]
LayoutTest

Clearing flags on attachment: 105073

Committed r93745: <http://trac.webkit.org/changeset/93745>
Comment 7 WebKit Review Bot 2011-08-24 15:36:11 PDT
All reviewed patches have been landed.  Closing bug.
Comment 8 Adam Barth 2011-08-24 16:37:55 PDT
Actually, Tom managed to repro this issue.  Instant was causing me not to be able to repro it before.
Comment 9 Daniel Bates 2011-08-24 16:46:47 PDT
(In reply to comment #8)
> Actually, Tom managed to repro this issue.  Instant was causing me not to be able to repro it before.

Can you elaborate?
Comment 10 Daniel Bates 2011-08-24 16:53:05 PDT
(In reply to comment #9)
> (In reply to comment #8)
> > Actually, Tom managed to repro this issue.  Instant was causing me not to be able to repro it before.
> 
> Can you elaborate?

Never mind I was able to reproduce this issue with both Chrome for Mac 13.0.782.215 and 14.0.835.109 beta using:

http://security.onofri.org/xss_location.html?#<script>alert('XS%41S');</script>

I am unable to reproduce this issue with either shipping Mac Safari 5.1 or Mac Nightly r93707.
Comment 11 Adam Barth 2011-08-24 16:59:54 PDT
> I am unable to reproduce this issue with either shipping Mac Safari 5.1 or Mac Nightly r93707.

That could be related to different URL handling in KURL and GURL.  Tom's thoughts are that we should try decoding these strings repeatedly until they stop changing, which should be robust to this sort of issue.
Comment 12 Daniel Bates 2011-08-24 17:09:49 PDT
Created attachment 105098 [details]
DRT Test Case

I was able to reproduce the issue with Safari when I load a URL that has a similar form as the example in an <iframe>.
Comment 13 Thomas Sepez 2011-08-29 10:30:07 PDT
Interestingly, the layout test that was introduced for this case is passing.  Here's what I'm observing:

The xss auditor bypass as reported requires that location.hash return a value of "#<script>alert('XS%41')</script>" to JS which is then passed to document.write().

Chrome (as distributed) and Safari (but only when launched from a fresh build via Tools/Script/run-safari") return this string.

Safari (as installed officially on my mac from cupertino) and (somehow) dumpRenderTree (as built) instead return "#%3cscript%3ealert('XS%41S')%3e/script%3c".

In order make this fire at all, the LayoutTest's .html file does document.write(unescape(...)).  The unescape introduces the needed <> punctuation, but also gets rid of the "%41", which must persist for this example to work.  Without a "%41" in the url, there isn't the conflict between xssauditor's decode() of URL and the page with the %41.

So ...  now I need to determine (if and) why say, alert(document.location) gives the different results for official safari vs. chrome vs. built safari vs. dumprendertree.
Comment 14 Daniel Bates 2011-08-29 12:51:52 PDT
Additional remarks:

As I remarked in comment 1, Safari URL encodes the fragment portion of the URL for the main frame load. In particular, '<', and '>' and URL encoded. So, #<script>alert('XS%41S');</script> becomes #%3Cscript%3Ealert('XS%41S');%3C/script%3E.

From debugging the DRT test case using an Apple Mac WebKit build, we don't encode the fragment portion of the <iframe>'s src attribute using the same encoding algorithm as we do for a main frame load. In particular, when we construct a KURL object from the value of the <iframe>'s src attribute in SubframeLoader::requestFrame() (http://trac.webkit.org/browser/trunk/Source/WebCore/loader/SubframeLoader.cpp?rev=85395#L81), we ultimately call escapeAndAppendFragment() (See remark (*)) in KURL.cpp which only escapes ASCII characters whose value is < 32 (Space) and >= 127 (DEL): <http://trac.webkit.org/browser/trunk/Source/WebCore/platform/KURL.cpp?rev=89336#L1017>.

For comparison, we use appendEscapingBadChars() in KURL.cpp to escape unsafe characters in the query string portion of the URL. And appendEscapingBadChars() escapes every isBadChar() character c \notin {'%', '?', '\t', '\n', '\r'}.

(*) Notice that HTMLFrameElementBase::openURL() calls SubframeLoader::requestFrame(); => SubframeLoader::completeURL() ;=> Document::completeURL(); => KURL(const KURL& base, const String& relative, const TextEncoding&) ;=> KURL::init(const KURL& base, const String& relative, const TextEncoding& encoding) ;=> KURL::parse(const char* url, const String* originalString); => escapeAndAppendFragment().
Comment 15 Thomas Sepez 2011-08-29 13:48:47 PDT
Ok, if that's the case, then we should be able to build a test that illustrates the issue if the unescape() is removed and a %41 is present in the hash.
Comment 16 Thomas Sepez 2011-08-29 14:15:15 PDT
Created attachment 105521 [details]
Revised test case showing the issue.
Comment 17 Thomas Sepez 2011-08-31 11:52:36 PDT
Created attachment 105806 [details]
Patch plus test case changes from prev attachment.

Reduces both the url/post parameters and the body snippet to a minimal form before making a comparison for a reflected XSS.
Comment 18 Adam Barth 2011-08-31 12:03:20 PDT
Comment on attachment 105806 [details]
Patch plus test case changes from prev attachment.

View in context: https://bugs.webkit.org/attachment.cgi?id=105806&action=review

This looks great.  Below are just some style nits.

> Source/WebCore/html/parser/XSSAuditor.cpp:119
> +static String fullyDecodeString(const String& string,
> +                                const TextResourceDecoder* decoder)

WebKit usually keeps function declarations to one line.

> Source/WebCore/html/parser/XSSAuditor.cpp:121
> +    size_t workingLen;

workingLen => workingLength

WebKit likes variable names make from complete words.

> Source/WebCore/html/parser/XSSAuditor.cpp:129
> +      CString workingStringUTF8 = workingString.utf8();
> +      String decodedString = decoder->encoding().decode(

4-space indent, pls.

> Source/WebCore/html/parser/XSSAuditor.cpp:130
> +          workingStringUTF8.data(), workingStringUTF8.length());

Also, this should be on one line.  There is no 80 column line limit in WebKit.

> Source/WebCore/html/parser/XSSAuditor.cpp:469
> +      return false;

4-space indent
Comment 19 Thomas Sepez 2011-08-31 12:22:59 PDT
Created attachment 105811 [details]
Patch from prev attachment with corrected indent style.

Sorry about that.  Looks like my editor settings were still set to "2" on this box.
Other question:  is there a way to prove this is sufficiently performant?  Thanks.
Comment 20 Adam Barth 2011-08-31 12:27:15 PDT
> Other question:  is there a way to prove this is sufficiently performant?  Thanks.

There are two approaches:

1) You can run the PLT.  You can either do this manually or watch http://build.chromium.org/f/chromium/perf/dashboard/overview.html once you patch rolls into Chromium.

2) We have some XSS auditor performance tests, but they're pretty minimal:

http://trac.webkit.org/browser/trunk/PerformanceTests/XSSAuditor

I don't think this patch will cause a performance problem, though.  The common case is that we'll decode the same number of times as before.
Comment 21 Adam Barth 2011-08-31 12:28:37 PDT
Comment on attachment 105811 [details]
Patch from prev attachment with corrected indent style.

View in context: https://bugs.webkit.org/attachment.cgi?id=105811&action=review

> Source/WebCore/html/parser/XSSAuditor.cpp:125
> +    } while (workingString.length() < workingLength);

After this program point, workingLength no longer reflects the length of workingString.  That's fine, but slightly confusing.  Maybe add a comment?  (Or maybe it's not worth worrying about.)
Comment 22 Thomas Sepez 2011-08-31 12:33:07 PDT
Created attachment 105814 [details]
Prev patch plus renamed length variable.

Sure, why not.
Comment 23 WebKit Review Bot 2011-08-31 13:51:19 PDT
Comment on attachment 105814 [details]
Prev patch plus renamed length variable.

Clearing flags on attachment: 105814

Committed r94225: <http://trac.webkit.org/changeset/94225>
Comment 24 WebKit Review Bot 2011-08-31 13:51:25 PDT
All reviewed patches have been landed.  Closing bug.