10
votes

I'm trying to get Flask to handle cross-site scripting properly. I've taken the crossdomain decorator snippet from here: http://flask.pocoo.org/snippets/56/

In the code below, I've put the decorator snippet and the basic flask server.

I'm calling the decorator with headers='Content-Type' because otherwise I was getting "Request header field Content-Type is not allowed by Access-Control-Allow-Headers." in the browser.

So here is my question: As-is, the code below works. But when I want to restrict to only a specific server like so:

@crossdomain(origin='myserver.com', headers='Content-Type')

I get the browser error

"Origin http://myserver.com is not allowed by Access-Control-Allow-Origin."

I can't get it working for anything other than origin='*'.

Does anybody have any ideas?

Here is the complete code:

from datetime import timedelta
from flask import make_response, request, current_app, Flask, jsonify
from functools import update_wrapper

def crossdomain(origin=None, methods=None, headers=None,
            max_age=21600, attach_to_all=True,
            automatic_options=True):
    if methods is not None:
        methods = ', '.join(sorted(x.upper() for x in methods))
    if headers is not None and not isinstance(headers, basestring):
        headers = ', '.join(x.upper() for x in headers)
    if not isinstance(origin, basestring):
        origin = ', '.join(origin)
    if isinstance(max_age, timedelta):
        max_age = max_age.total_seconds()

    def get_methods():
        if methods is not None:
            return methods

        options_resp = current_app.make_default_options_response()
        return options_resp.headers['allow']

    def decorator(f):
        def wrapped_function(*args, **kwargs):
            if automatic_options and request.method == 'OPTIONS':
            resp = current_app.make_default_options_response()
            else:
                resp = make_response(f(*args, **kwargs))
            if not attach_to_all and request.method != 'OPTIONS':
                return resp

            h = resp.headers

            h['Access-Control-Allow-Origin'] = origin
            h['Access-Control-Allow-Methods'] = get_methods()
            h['Access-Control-Max-Age'] = str(max_age)
            if headers is not None:
                h['Access-Control-Allow-Headers'] = headers
            return resp

        f.provide_automatic_options = False
        return update_wrapper(wrapped_function, f)
    return decorator

app = Flask(__name__)

@app.route('/my_service', methods=['POST', 'OPTIONS'])
@crossdomain(origin='*', headers='Content-Type')
def my_service():
    return jsonify(foo='cross domain ftw')

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=8080, debug=True)

For reference my python version is 2.7.2 Flask version is 0.7.2

2

2 Answers

2
votes

I just tried the same code with python version 2.7.3 and Flask version 0.8.

With these versions, it fails with

@crossdomain(origin='myserver.com', headers='Content-Type')

but it works with

@crossdomain(origin='http://myserver.com', headers='Content-Type')

Perhaps it just doesn't work with Flask 0.7.2? (despite what it says on the snippet page).


EDIT: After playing with this a lot more (and upgrading to Flask 0.9) it seems that the real problem (or yet another problem) might be related to having multiple allowed origins in a list. In other words, using the above code like this:

@crossdomain(origin=['http://myserver.com', 'http://myserver2.com'], headers='Content-Type')

doesn't work.

To fix this problem I tweaked the decorator. See code here: http://chopapp.com/#351l7gc3

This code returns only the domain of the requesting site if it is in the list. Kinda quirky, but at least for me, problem solved :)

1
votes

Python is trying hard to prevent you from exposing yourself to cross site scripting attacks.

One fix is by giving in, and having your requests hit the same server the flask script is running on. Fetching JSON from far away servers defined in strings is risky business anyway.

I was able to fix it by letting the browser keep itself on the same server, like this:

$('a#calculate').bind('click', function() {
  $.getJSON('/_add_numbers', { 
    a: $('input[name="a"]').val(),
    b: $('input[name="b"]').val()
  }, function(data) {
    $("#result").text(data.request);
  });
  return false;
});

Notice how getJSON method is passed a /_add_numbers. That communicates to the browser to stay on the same host and look for that page. Then the browser is happy and secure we are staying on the same host, and you never get the error:

Origin http://myserver.com is not allowed by Access-Control-Allow-Origin