CVE-2013-0270 (published 2013-04-12T22:55Z)

OpenStack Keystone Grizzly before 2013.1, Folsom, and possibly earlier allows remote attackers to cause a denial of service (CPU and memory consumption) via a large HTTP request, as demonstrated by a long tenant_name when requesting a token.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:N/A:P)

Weakness information:
cwe_id: CWE-119
cwe_name: Improper Restriction of Operations within the Bounds of a Memory Buffer
description: The product performs operations on a memory buffer, but it reads from or writes to a memory location outside the buffer's intended boundary. This may result in read or write operations on unexpected memory locations that could be linked to other variables, data structures, or internal program data.
extended_description: 
            Certain languages allow direct addressing of memory locations and do not automatically ensure that these locations are valid for the memory buffer that is being referenced. This can cause read or write operations to be performed on memory locations that may be associated with other variables, data structures, or internal program data.
         
         

Fix information:
repo_url: https://github.com/openstack/keystone
commit_details: repo_url: https://github.com/openstack/keystone
author: Dan Prince
author_date: 2013-01-12 22:22:42-05:00
msg: 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
repository: repo_url: https://github.com/openstack/keystone
repo_name: openstack/keystone
description: OpenStack Identity (Keystone). Mirror of code maintained at opendev.org.
repo_language: Python
owner: openstack
file_changes: filename: keystone.conf.sample
change_type: ModificationType.MODIFY
diff: @@ -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

programming_language: Ruby
filename: utils.py
change_type: ModificationType.MODIFY
diff: @@ -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

programming_language: Python
filename: config.py
change_type: ModificationType.MODIFY
diff: @@ -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)

programming_language: Python
filename: exception.py
change_type: ModificationType.MODIFY
diff: @@ -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.
 

programming_language: Python
filename: core.py
change_type: ModificationType.MODIFY
diff: @@ -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

programming_language: Python
filename: test_sizelimit.py
change_type: ModificationType.ADD
diff: @@ -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)

programming_language: Python