alf.nu / @steike

Safari Reader UXSS

Safari Reader is Apple's clone-slash-port of Readability, and "allows users to read online articles in a continuous, clutter-free view, with no ads or visual distractions. Reader concatenates multipage articles into a single scrolling pane."

If you're on (an unpatched) Safari right now, click the ≡ at the left of the address bar for a demo. If not, this is what it used to look like:

What?

Safari Reader copies the text on the page into a separate document loaded under a safari-reader:// origin. It attempts to remove active content, but fails in at least two ways:

Normally this would not be a problem (oh no, the site can run script on itself!), but the safari-reader:// origin comes with some unique abilities. Reader has a nice feature that automatically concatenates multi-page documents. This could have been implemented by fetching and parsing the HTML, but being in the browser and all, it instead loads the next page in a hidden iframe and grabs the content from the resulting DOM. This has advantages (maybe the site needs JS to render the page) and disadvantages (the site could have X-Frame-Options: DENY and fail), but it was a reasonable choice for a bookmarklet.

After moving the content to safari-reader://, this trick no longer works – the DOM of https://example.com/ is in a separate origin and inaccessible to javascript running on safari-reader://example.com/. To make it work, ReaderJSController calls a native method that (deep breath) grabs the document, without checking the origin. If you're in an old Safari, the following simple code will steal the contents of example.com (and you can run it by activating Reader on this page, courtesy of this little iframe: '')


  f = document.createElement('iframe');
  f.id = 'foo';
  f.onload = function() {
    ReaderJSController.prepareNextPageFrame('foo');
    alert(ReaderJSController.nextPageArticleFinder().contentDocument.body.outerHTML);
  };
  f.src = 'https://example.com/';
  document.body.appendChild(f);

In addition to stealing the contents, you can document.write() to run arbitrary JavaScript in the remote DOM.

iOS

The same code runs on iOS, with the added bonus that a malicious or hijacked app can invoke it via SFSafariViewController without user interaction.

Response

2016-04-05: Original report.

2016-09-20: Safari 10 and iOS 10 released with fix for "CVE-2016-4618?: Enabling the Safari Reader feature on a maliciously crafted webpage may lead to universal cross site scripting. Multiple validation issues were addressed through improved input sanitization." It now rejects any iframe with a srcdoc attribute, and calls trim() and toLowerCase() before checking if the URL starts with javascript:.

2016-09-22: Reported another filtering bug; recommended neutering ReaderJSController instead of trying to fix the filtering.

2016-12-12: iOS 10.2 released with fix for "CVE-2016-7650?: Enabling the Safari Reader feature on a maliciously crafted webpage may lead to universal cross site scripting. Multiple validation issues were addressed through improved input sanitization."

2016-12-13: Safari 10.0.2 released.

2016-12-13: Reported another filtering bug; recommended neutering ReaderJSController instead of trying to fix the filtering.

2017-03-21: Beta version released.

2017-03-27: iOS 10.3 and Safari 10 released with fix for "CVE-2017-2393?: Enabling the Safari Reader feature on a maliciously crafted webpage may lead to universal cross site scripting." It is still possible to run JavaScript in Reader mode, but that script is no longer allowed to load cross-origin iframes.

Test cases

Safari Reader Tweet Safari Reader Tweet V2 Safari Reader Tweet V3 Safari Reader Tweet V4

Complaints to @steike or @steike.