DXF is a fast DOM-Based XSS filter that runs entirely in JavaScript using ES6 features. Unlike other filters, it does not require modifications to the JavaScript engine. It also does not perform intrusive source-to-source transformations that increase page loading time or runtime overhead.
DXF currently only runs in Firefox, but it uses standard ES6 features available in Chrome and IE >= 10.
DXF monitors all JavaScript sources (e.g. document.location
) and sinks (e.g.
document.write
). Before sinks are executed, taint-inteference is used to
infer a flow between any of the sources and that particular sink. If the flow
generates new JavaScript code, the filter prevents the sink from executing.
Sources are monitored because, although we cannot track flows explicitly, we
can immediately exclude any sources that have not been touched from the
matching process. In this sense, source monitoring is more of an optimization
than a requirement.
Although the filter can be implemented as an HTTP proxy, we implement it here as a Firefox extension using the add-on SDK.
Install the following:
- emscripten
- node >=5.1.1
- babel
- uglifyjs
Then run make xpi
and drag&drop the XPI into your FF42 installation.
The filter is made of the following components:
-
Rewriter (
extension/rewriter.js
): implements code rewriting, to rename all references to objects that lead to sources and sinks which cannot be otherwise monitored at runtime (e.g. non-configurable properties that cannot be wrapped, likewindow
,document
,location
, or non- wrappeable property likeeval
because of direct eval semantics). The runtime will then be able to monitor the rewritten reference (e.g. by using a proxy on__window
). -
Proxy (
extension/index.js / addon-proxy
): uses the rewriter on all incoming HTML and JS files. Additionally, it exports some functionality from the proxy extension to web content. This functionality could also be exported separately to web content with a decent build system, but that's how it is now. -
Runtime Monitoring (
setup.js / utils.js
): these scripts execute before any other scripts on the page. They wrap all configurable sources and sinks, and define proxies for the non-configurable ones, which the rewriter has redirected (e.g.window
to__window
). The purpose of this wrapping is to intercept sinks and check for XSS attacks. -
Taint-Inference Engine (
p1.js / p1utils.js
):p1.js
is an asm.js module based onnlearn
, an approximate substring matching routine used in XSSFilt. ``p1utils.jsexports the high-level function
p1FastMatch`.
Here we detail the main sources and sinks and explain how we interpose on them (roughly based on the DOMXSSWiki).
Sources:
-
document.(URL|documentURI)
: equivalent to the URL of the page, which is always relevant for DOM-Based XSS, so it's always included. Wrapped as a configurable property ofDocument.prototype
-
document.baseURI
: wrappable as a configurable property ofNode.prototype
(every node has a baseURI), but is it really a legitimate sink? If you can modify the base URL through reflected XSS, you have bigger fishes to fry. -
location(.(href|search|hash|pathname))?
:location
is a non-configurable property ofdocument
andwindow
. The only way to interpose on it is to rewritedocument
,window
andlocation
itself to use proxies. Gettinglocation.href
is the same asdocument.URL
, which is always included. -
document.cookie
: wrappable, configurable prop ofHTMLDocument.prototype
. -
document.referrer
: wrappable, configurable prop ofDocument.prototype
. -
window.name
: wrappable, configurable prop ofwindow
. -
history
: wrappable, configurable prop ofwindow
. It can affectlocation.href
, but do we care about tracking the value of that over time? TODO -
localStorage
: wrappable, configurable. TODO -
sessionStorage
: wrappable, configurable. TODO -
onmessage - event.data
: configurable onMessageEvent.prototype
, but i am wondering if this is only necessary in limited cases. Same-origin messages can be excluded for example. TODO
Sinks:
-
eval
: configurable onwindow
, but it cannot be wrapped without breaking direct eval semantics. direct evals will use__peval
and indirect evals will use__indirectEval
, and__window.eval
will redirect to__inidirectEval
. -
Function
: wrappeable, configurable. -
setTimeout/setInterval
: wrappable, configurable. -
scriptEl.src
: wrappeable, configurable onHTMLScriptElement.prototype
. -
scriptEl.textContent
: wrappeable, configurable onNode.prototype
. -
scriptEl.innerText
: not a sink in FF. -
location(.(href|search|hash|pathname))?
:location
is non-configurable onwindow
anddocument
, and each one of its members are non-configurable. Requires rewriting and proxying ofwindow
,document
,location
. Onlyhref
andsearch
are valid vectors for now, because they need to inject "javascript:" at the beginning. -
document.(write|writeln)
: wrappeable, configurable onHTMLDocument.prototype
. -
el.innerHTML
: wrappeable, configurable onElement.prototype
. -
Range
: can create document fragments, but they still need to be appended. Probably does not need to be addressed directly. NOT NECESSARY?
Other problems:
-
parent/top/frames[i].obj
:parent
andframes
are wrapped, top is rewritten as__top
. -
self
: wrappeable, configurable onwindow
. -
iframeEl.contentWindow
: direct access to other frames'window
. Not only access needs to be redirected to__window
, but in some cases the environment itself must be initialized (e.g.about:blank
). Wrappeable, configurable onHTMLIFrameElement.prototype
. -
window.window
: direct access towindow
. non-configurable, __window must intercept it.