HTTP: CSRF protection for RPC endpoint.
By now enforcing the Content-Type header is set to 'application/json', we force browsers attempting a cross-domain request to first perform a CORS preflight OPTIONS request. This request always includes an Origin header which we check against our whitelist. The whitelist contains the current Host as well as anything specified in the new optional allowed_origins config value. Any non-browser tools must also now set the Context-type header.
This commit is contained in:
parent
41882c6395
commit
cd829c7042
@ -25,6 +25,7 @@ class Extension(ext.Extension):
|
|||||||
schema['port'] = config_lib.Port()
|
schema['port'] = config_lib.Port()
|
||||||
schema['static_dir'] = config_lib.Path(optional=True)
|
schema['static_dir'] = config_lib.Path(optional=True)
|
||||||
schema['zeroconf'] = config_lib.String(optional=True)
|
schema['zeroconf'] = config_lib.String(optional=True)
|
||||||
|
schema['allowed_origins'] = config_lib.List(optional=True)
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def validate_environment(self):
|
def validate_environment(self):
|
||||||
|
|||||||
@ -4,3 +4,4 @@ hostname = 127.0.0.1
|
|||||||
port = 6680
|
port = 6680
|
||||||
static_dir =
|
static_dir =
|
||||||
zeroconf = Mopidy HTTP server on $hostname
|
zeroconf = Mopidy HTTP server on $hostname
|
||||||
|
allowed_origins =
|
||||||
|
|||||||
@ -11,20 +11,24 @@ import tornado.websocket
|
|||||||
|
|
||||||
import mopidy
|
import mopidy
|
||||||
from mopidy import core, models
|
from mopidy import core, models
|
||||||
|
from mopidy.compat import urllib
|
||||||
from mopidy.internal import encoding, jsonrpc
|
from mopidy.internal import encoding, jsonrpc
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def make_mopidy_app_factory(apps, statics):
|
def make_mopidy_app_factory(apps, statics):
|
||||||
def mopidy_app_factory(config, core):
|
def mopidy_app_factory(config, core):
|
||||||
|
origin_whitelist = {
|
||||||
|
x.lower() for x in config['http']['allowed_origins'] if x
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
(r'/ws/?', WebSocketHandler, {
|
(r'/ws/?', WebSocketHandler, {
|
||||||
'core': core,
|
'core': core,
|
||||||
}),
|
}),
|
||||||
(r'/rpc', JsonRpcHandler, {
|
(r'/rpc', JsonRpcHandler, {
|
||||||
'core': core,
|
'core': core,
|
||||||
|
'origin_whitelist': origin_whitelist,
|
||||||
}),
|
}),
|
||||||
(r'/(.+)', StaticFileHandler, {
|
(r'/(.+)', StaticFileHandler, {
|
||||||
'path': os.path.join(os.path.dirname(__file__), 'data'),
|
'path': os.path.join(os.path.dirname(__file__), 'data'),
|
||||||
@ -143,16 +147,31 @@ def set_mopidy_headers(request_handler):
|
|||||||
'X-Mopidy-Version', mopidy.__version__.encode('utf-8'))
|
'X-Mopidy-Version', mopidy.__version__.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def check_origin(origin, request_headers, origin_whitelist):
|
||||||
|
if origin is None:
|
||||||
|
logger.debug('Origin was not set')
|
||||||
|
return False
|
||||||
|
origin_whitelist.add(request_headers.get('Host', None))
|
||||||
|
parsed_origin = urllib.parse.urlparse(origin).netloc.lower()
|
||||||
|
return parsed_origin and parsed_origin in origin_whitelist
|
||||||
|
|
||||||
|
|
||||||
class JsonRpcHandler(tornado.web.RequestHandler):
|
class JsonRpcHandler(tornado.web.RequestHandler):
|
||||||
|
|
||||||
def initialize(self, core):
|
def initialize(self, core, origin_whitelist):
|
||||||
self.jsonrpc = make_jsonrpc_wrapper(core)
|
self.jsonrpc = make_jsonrpc_wrapper(core)
|
||||||
|
self.origin_whitelist = origin_whitelist
|
||||||
|
|
||||||
def head(self):
|
def head(self):
|
||||||
self.set_extra_headers()
|
self.set_extra_headers()
|
||||||
self.finish()
|
self.finish()
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
|
content_type = self.request.headers.get("Content-Type", '')
|
||||||
|
if content_type != 'application/json':
|
||||||
|
self.set_status(406, 'Content-Type must be application/json')
|
||||||
|
return
|
||||||
|
|
||||||
data = self.request.body
|
data = self.request.body
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
@ -177,6 +196,18 @@ class JsonRpcHandler(tornado.web.RequestHandler):
|
|||||||
self.set_header('Accept', 'application/json')
|
self.set_header('Accept', 'application/json')
|
||||||
self.set_header('Content-Type', 'application/json; utf-8')
|
self.set_header('Content-Type', 'application/json; utf-8')
|
||||||
|
|
||||||
|
def options(self):
|
||||||
|
origin = self.request.headers.get('Origin', None)
|
||||||
|
if not check_origin(origin, self.request.headers,
|
||||||
|
self.origin_whitelist):
|
||||||
|
self.set_status(403, 'Access denied for origin %s' % origin)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.set_header("Access-Control-Allow-Origin", "%s" % origin)
|
||||||
|
self.set_header("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
self.set_status(204)
|
||||||
|
self.finish()
|
||||||
|
|
||||||
|
|
||||||
class ClientListHandler(tornado.web.RequestHandler):
|
class ClientListHandler(tornado.web.RequestHandler):
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user