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
This commit is contained in:
Dan Prince 2013-01-12 22:22:42 -05:00
parent 8748cfa3a6
commit 7691276b86
6 changed files with 127 additions and 5 deletions

View File

@ -186,6 +186,9 @@ paste.filter_factory = keystone.contrib.s3:S3Extension.factory
[filter:url_normalize] [filter:url_normalize]
paste.filter_factory = keystone.middleware:NormalizingFilter.factory paste.filter_factory = keystone.middleware:NormalizingFilter.factory
[filter:sizelimit]
paste.filter_factory = keystone.middleware:RequestBodySizeLimiter.factory
[filter:stats_monitoring] [filter:stats_monitoring]
paste.filter_factory = keystone.contrib.stats:StatsMiddleware.factory 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 paste.app_factory = keystone.service:admin_app_factory
[pipeline:public_api] [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: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: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] [app:public_version_service]
paste.app_factory = keystone.service:public_version_app_factory 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 paste.app_factory = keystone.service:admin_version_app_factory
[pipeline:public_version_api] [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: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] [composite:main]
use = egg:Paste#urlmap use = egg:Paste#urlmap

View File

@ -311,3 +311,37 @@ def setup_remote_pydev_debug():
except: except:
LOG.exception(_(error_msg)) LOG.exception(_(error_msg))
raise 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

View File

@ -137,6 +137,8 @@ register_str('onready')
register_str('auth_admin_prefix', default='') register_str('auth_admin_prefix', default='')
register_str('policy_file', default='policy.json') register_str('policy_file', default='policy.json')
register_str('policy_default_rule', default=None) register_str('policy_default_rule', default=None)
#default max request size is 112k
register_int('max_request_body_size', default=114688)
#ssl options #ssl options
register_bool('enable', group='ssl', default=False) register_bool('enable', group='ssl', default=False)

View File

@ -173,6 +173,12 @@ class Conflict(Error):
title = 'Conflict' title = 'Conflict'
class RequestTooLarge(Error):
"""Request is too large."""
code = 413
title = 'Request is too large.'
class UnexpectedError(Error): class UnexpectedError(Error):
"""An unexpected error prevented the server from fulfilling your request. """An unexpected error prevented the server from fulfilling your request.

View File

@ -14,7 +14,10 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import webob.dec
from keystone.common import serializer from keystone.common import serializer
from keystone.common import utils
from keystone.common import wsgi from keystone.common import wsgi
from keystone import config from keystone import config
from keystone import exception from keystone import exception
@ -164,3 +167,21 @@ class NormalizingFilter(wsgi.Middleware):
# Rewrites path to root if no path is given. # Rewrites path to root if no path is given.
elif not request.environ['PATH_INFO']: elif not request.environ['PATH_INFO']:
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

56
tests/test_sizelimit.py Normal file
View File

@ -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)