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:
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:
javascript:
are filtered, but ones with JaVASCRiPT:
or javascript:
are not.
Embedded iframes are allowed if the URL has a domain with youtube
as a substring, like this:
<iframe src="//youtube.invalid/" srcdoc="<script>console.log(document.domain)</script>"> </iframe>
Note that src
is ignored by the browser if srcdoc
is present.
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.
The same code runs on iOS, with the added bonus that a malicious or
hijacked app can invoke it via SFSafariViewController
without user
interaction.
2016-04-05: Original report.
2016-09-20: Safari 10 and iOS 10 released with fix for "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 "
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 "