From 7691276b869a86c2b75631d5bede9f61e030d9d8 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Sat, 12 Jan 2013 22:22:42 -0500 Subject: [PATCH] Limit the size of HTTP requests. Adds a new RequestBodySizeLimiter middleware to guard against really large HTTP requests. The default max request size is 112k although this limit is configurable via the 'max_request_body_size' config parameter. Fixes LP Bug #1099025. Change-Id: Id51be3d9a0d829d63d55a92dca61a39a17629785 --- etc/keystone.conf.sample | 13 +++++---- keystone/common/utils.py | 34 ++++++++++++++++++++++ keystone/config.py | 2 ++ keystone/exception.py | 6 ++++ keystone/middleware/core.py | 21 ++++++++++++++ tests/test_sizelimit.py | 56 +++++++++++++++++++++++++++++++++++++ 6 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 tests/test_sizelimit.py diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index 13a78475db..4017a04d4f 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -186,6 +186,9 @@ paste.filter_factory = keystone.contrib.s3:S3Extension.factory [filter:url_normalize] paste.filter_factory = keystone.middleware:NormalizingFilter.factory +[filter:sizelimit] +paste.filter_factory = keystone.middleware:RequestBodySizeLimiter.factory + [filter:stats_monitoring] paste.filter_factory = keystone.contrib.stats:StatsMiddleware.factory @@ -202,13 +205,13 @@ paste.app_factory = keystone.service:v3_app_factory paste.app_factory = keystone.service:admin_app_factory [pipeline:public_api] -pipeline = stats_monitoring url_normalize token_auth admin_token_auth xml_body json_body debug ec2_extension user_crud_extension public_service +pipeline = sizelimit stats_monitoring url_normalize token_auth admin_token_auth xml_body json_body debug ec2_extension user_crud_extension public_service [pipeline:admin_api] -pipeline = stats_monitoring url_normalize token_auth admin_token_auth xml_body json_body debug stats_reporting ec2_extension s3_extension crud_extension admin_service +pipeline = sizelimit stats_monitoring url_normalize token_auth admin_token_auth xml_body json_body debug stats_reporting ec2_extension s3_extension crud_extension admin_service [pipeline:api_v3] -pipeline = stats_monitoring url_normalize token_auth admin_token_auth xml_body json_body debug stats_reporting ec2_extension s3_extension service_v3 +pipeline = sizelimit stats_monitoring url_normalize token_auth admin_token_auth xml_body json_body debug stats_reporting ec2_extension s3_extension service_v3 [app:public_version_service] paste.app_factory = keystone.service:public_version_app_factory @@ -217,10 +220,10 @@ paste.app_factory = keystone.service:public_version_app_factory paste.app_factory = keystone.service:admin_version_app_factory [pipeline:public_version_api] -pipeline = stats_monitoring url_normalize xml_body public_version_service +pipeline = sizelimit stats_monitoring url_normalize xml_body public_version_service [pipeline:admin_version_api] -pipeline = stats_monitoring url_normalize xml_body admin_version_service +pipeline = sizelimit stats_monitoring url_normalize xml_body admin_version_service [composite:main] use = egg:Paste#urlmap diff --git a/keystone/common/utils.py b/keystone/common/utils.py index d74da5b548..2c194db519 100644 --- a/keystone/common/utils.py +++ b/keystone/common/utils.py @@ -311,3 +311,37 @@ def setup_remote_pydev_debug(): except: LOG.exception(_(error_msg)) raise + + +class LimitingReader(object): + """Reader to limit the size of an incoming request.""" + def __init__(self, data, limit): + """ + :param data: Underlying data object + :param limit: maximum number of bytes the reader should allow + """ + self.data = data + self.limit = limit + self.bytes_read = 0 + + def __iter__(self): + for chunk in self.data: + self.bytes_read += len(chunk) + if self.bytes_read > self.limit: + raise exception.RequestTooLarge() + else: + yield chunk + + def read(self, i): + result = self.data.read(i) + self.bytes_read += len(result) + if self.bytes_read > self.limit: + raise exception.RequestTooLarge() + return result + + def read(self): + result = self.data.read() + self.bytes_read += len(result) + if self.bytes_read > self.limit: + raise exception.RequestTooLarge() + return result diff --git a/keystone/config.py b/keystone/config.py index c26a518c24..72fd0dcb54 100644 --- a/keystone/config.py +++ b/keystone/config.py @@ -137,6 +137,8 @@ register_str('onready') register_str('auth_admin_prefix', default='') register_str('policy_file', default='policy.json') register_str('policy_default_rule', default=None) +#default max request size is 112k +register_int('max_request_body_size', default=114688) #ssl options register_bool('enable', group='ssl', default=False) diff --git a/keystone/exception.py b/keystone/exception.py index 26697e0db4..2787e064aa 100644 --- a/keystone/exception.py +++ b/keystone/exception.py @@ -173,6 +173,12 @@ class Conflict(Error): title = 'Conflict' +class RequestTooLarge(Error): + """Request is too large.""" + code = 413 + title = 'Request is too large.' + + class UnexpectedError(Error): """An unexpected error prevented the server from fulfilling your request. diff --git a/keystone/middleware/core.py b/keystone/middleware/core.py index a49f743be4..24495c9879 100644 --- a/keystone/middleware/core.py +++ b/keystone/middleware/core.py @@ -14,7 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. +import webob.dec + from keystone.common import serializer +from keystone.common import utils from keystone.common import wsgi from keystone import config from keystone import exception @@ -164,3 +167,21 @@ class NormalizingFilter(wsgi.Middleware): # Rewrites path to root if no path is given. elif not request.environ['PATH_INFO']: request.environ['PATH_INFO'] = '/' + + +class RequestBodySizeLimiter(wsgi.Middleware): + """Limit the size of an incoming request.""" + + def __init__(self, *args, **kwargs): + super(RequestBodySizeLimiter, self).__init__(*args, **kwargs) + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + + if req.content_length > CONF.max_request_body_size: + raise exception.RequestTooLarge() + if req.content_length is None and req.is_body_readable: + limiter = utils.LimitingReader(req.body_file, + CONF.max_request_body_size) + req.body_file = limiter + return self.application diff --git a/tests/test_sizelimit.py b/tests/test_sizelimit.py new file mode 100644 index 0000000000..aec57ecfe3 --- /dev/null +++ b/tests/test_sizelimit.py @@ -0,0 +1,56 @@ +# Copyright (c) 2013 OpenStack, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob + +from keystone import config +from keystone import exception +from keystone import middleware +from keystone import test + +CONF = config.CONF +MAX_REQUEST_BODY_SIZE = CONF.max_request_body_size + + +class TestRequestBodySizeLimiter(test.TestCase): + + def setUp(self): + super(TestRequestBodySizeLimiter, self).setUp() + + @webob.dec.wsgify() + def fake_app(req): + return webob.Response(req.body) + + self.middleware = middleware.RequestBodySizeLimiter(fake_app) + self.request = webob.Request.blank('/', method='POST') + + def test_content_length_acceptable(self): + self.request.headers['Content-Length'] = MAX_REQUEST_BODY_SIZE + self.request.body = "0" * MAX_REQUEST_BODY_SIZE + response = self.request.get_response(self.middleware) + self.assertEqual(response.status_int, 200) + + def test_content_length_too_large(self): + self.request.headers['Content-Length'] = MAX_REQUEST_BODY_SIZE + 1 + self.request.body = "0" * (MAX_REQUEST_BODY_SIZE + 1) + self.assertRaises(exception.RequestTooLarge, + self.request.get_response, + self.middleware) + + def test_request_too_large_no_content_length(self): + self.request.body = "0" * (MAX_REQUEST_BODY_SIZE + 1) + self.request.headers['Content-Length'] = None + self.assertRaises(exception.RequestTooLarge, + self.request.get_response, + self.middleware)