only allow methods which implement HTTP verbs to be called remotely

This fixes 500 server crashes caused by requests such as:

curl -X__init__ "http://your-swift-object-server:6000/sda1/p/a/c/o"

Fixes bug 1005903

Change-Id: I6c0ad39a29e07ce5f46b0fdbd11a53a9a1010a04
This commit is contained in:
Ionuț Arțăriși 2012-06-01 16:39:35 +02:00
parent 783f16035a
commit 9f5a6bba1a
9 changed files with 147 additions and 32 deletions

View File

@ -31,7 +31,7 @@ import simplejson
import swift.common.db
from swift.common.db import AccountBroker
from swift.common.utils import get_logger, get_param, hash_path, \
from swift.common.utils import get_logger, get_param, hash_path, public, \
normalize_timestamp, split_path, storage_directory, TRUE_VALUES
from swift.common.constraints import ACCOUNT_LISTING_LIMIT, \
check_mount, check_float, check_utf8
@ -63,6 +63,7 @@ class AccountController(object):
db_path = os.path.join(self.root, drive, db_dir, hsh + '.db')
return AccountBroker(db_path, account=account, logger=self.logger)
@public
def DELETE(self, req):
"""Handle HTTP DELETE request."""
start_time = time.time()
@ -88,6 +89,7 @@ class AccountController(object):
self.logger.timing_since('DELETE.timing', start_time)
return HTTPNoContent(request=req)
@public
def PUT(self, req):
"""Handle HTTP PUT request."""
start_time = time.time()
@ -149,6 +151,7 @@ class AccountController(object):
else:
return HTTPAccepted(request=req)
@public
def HEAD(self, req):
"""Handle HTTP HEAD request."""
# TODO(refactor): The account server used to provide a 'account and
@ -192,6 +195,7 @@ class AccountController(object):
self.logger.timing_since('HEAD.timing', start_time)
return HTTPNoContent(request=req, headers=headers)
@public
def GET(self, req):
"""Handle HTTP GET request."""
start_time = time.time()
@ -292,6 +296,7 @@ class AccountController(object):
self.logger.timing_since('GET.timing', start_time)
return ret
@public
def REPLICATE(self, req):
"""
Handle HTTP REPLICATE request.
@ -318,6 +323,7 @@ class AccountController(object):
self.logger.timing_since('REPLICATE.timing', start_time)
return ret
@public
def POST(self, req):
"""Handle HTTP POST request."""
start_time = time.time()
@ -357,10 +363,14 @@ class AccountController(object):
res = HTTPPreconditionFailed(body='Invalid UTF8')
else:
try:
if hasattr(self, req.method):
res = getattr(self, req.method)(req)
else:
# disallow methods which are not publicly accessible
try:
method = getattr(self, req.method)
getattr(method, 'publicly_accessible')
except AttributeError:
res = HTTPMethodNotAllowed()
else:
res = method(req)
except (Exception, Timeout):
self.logger.exception(_('ERROR __call__ error with %(method)s'
' %(path)s '), {'method': req.method, 'path': req.path})

View File

@ -1246,3 +1246,18 @@ def streq_const_time(s1, s2):
for (a, b) in zip(s1, s2):
result |= ord(a) ^ ord(b)
return result == 0
def public(func):
"""
Decorator to declare which methods are publicly accessible as HTTP
requests
:param func: function to make public
"""
func.publicly_accessible = True
@functools.wraps(func)
def wrapped(*a, **kw):
return func(*a, **kw)
return wrapped

View File

@ -31,7 +31,7 @@ from webob.exc import HTTPAccepted, HTTPBadRequest, HTTPConflict, \
import swift.common.db
from swift.common.db import ContainerBroker
from swift.common.utils import get_logger, get_param, hash_path, \
from swift.common.utils import get_logger, get_param, hash_path, public, \
normalize_timestamp, storage_directory, split_path, validate_sync_to, \
TRUE_VALUES
from swift.common.constraints import CONTAINER_LISTING_LIMIT, \
@ -138,6 +138,7 @@ class ContainerController(object):
'device': account_device})
return None
@public
def DELETE(self, req):
"""Handle HTTP DELETE request."""
start_time = time.time()
@ -187,6 +188,7 @@ class ContainerController(object):
return HTTPNoContent(request=req)
return HTTPNotFound()
@public
def PUT(self, req):
"""Handle HTTP PUT request."""
start_time = time.time()
@ -255,6 +257,7 @@ class ContainerController(object):
else:
return HTTPAccepted(request=req)
@public
def HEAD(self, req):
"""Handle HTTP HEAD request."""
start_time = time.time()
@ -288,6 +291,7 @@ class ContainerController(object):
self.logger.timing_since('HEAD.timing', start_time)
return HTTPNoContent(request=req, headers=headers)
@public
def GET(self, req):
"""Handle HTTP GET request."""
start_time = time.time()
@ -409,6 +413,7 @@ class ContainerController(object):
self.logger.timing_since('GET.timing', start_time)
return ret
@public
def REPLICATE(self, req):
"""
Handle HTTP REPLICATE request (json-encoded RPC calls for replication.)
@ -434,6 +439,7 @@ class ContainerController(object):
self.logger.timing_since('REPLICATE.timing', start_time)
return ret
@public
def POST(self, req):
"""Handle HTTP POST request."""
start_time = time.time()
@ -485,10 +491,14 @@ class ContainerController(object):
res = HTTPPreconditionFailed(body='Invalid UTF8')
else:
try:
if hasattr(self, req.method):
res = getattr(self, req.method)(req)
else:
# disallow methods which have not been marked 'public'
try:
method = getattr(self, req.method)
getattr(method, 'publicly_accessible')
except AttributeError:
res = HTTPMethodNotAllowed()
else:
res = method(req)
except (Exception, Timeout):
self.logger.exception(_('ERROR __call__ error with %(method)s'
' %(path)s '), {'method': req.method, 'path': req.path})

View File

@ -35,7 +35,7 @@ from webob.exc import HTTPAccepted, HTTPBadRequest, HTTPCreated, \
from xattr import getxattr, setxattr
from eventlet import sleep, Timeout, tpool
from swift.common.utils import mkdirs, normalize_timestamp, \
from swift.common.utils import mkdirs, normalize_timestamp, public, \
storage_directory, hash_path, renamer, fallocate, \
split_path, drop_buffer_cache, get_logger, write_pickle
from swift.common.bufferedhttp import http_connect
@ -484,6 +484,7 @@ class ObjectController(object):
'%s-%s/%s/%s' % (delete_at, account, container, obj),
host, partition, contdevice, headers_out, objdevice)
@public
def POST(self, request):
"""Handle HTTP POST requests for the Swift Object Server."""
start_time = time.time()
@ -543,6 +544,7 @@ class ObjectController(object):
self.logger.timing_since('POST.timing', start_time)
return response_class(request=request)
@public
def PUT(self, request):
"""Handle HTTP PUT requests for the Swift Object Server."""
start_time = time.time()
@ -641,6 +643,7 @@ class ObjectController(object):
self.logger.timing_since('PUT.timing', start_time)
return resp
@public
def GET(self, request):
"""Handle HTTP GET requests for the Swift Object Server."""
start_time = time.time()
@ -729,6 +732,7 @@ class ObjectController(object):
self.logger.timing_since('GET.timing', start_time)
return request.get_response(response)
@public
def HEAD(self, request):
"""Handle HTTP HEAD requests for the Swift Object Server."""
start_time = time.time()
@ -774,6 +778,7 @@ class ObjectController(object):
self.logger.timing_since('HEAD.timing', start_time)
return response
@public
def DELETE(self, request):
"""Handle HTTP DELETE requests for the Swift Object Server."""
start_time = time.time()
@ -824,6 +829,7 @@ class ObjectController(object):
self.logger.timing_since('DELETE.timing', start_time)
return resp
@public
def REPLICATE(self, request):
"""
Handle REPLICATE requests for the Swift Object Server. This is used
@ -862,10 +868,14 @@ class ObjectController(object):
res = HTTPPreconditionFailed(body='Invalid UTF8')
else:
try:
if hasattr(self, req.method):
res = getattr(self, req.method)(req)
else:
# disallow methods which have not been marked 'public'
try:
method = getattr(self, req.method)
getattr(method, 'publicly_accessible')
except AttributeError:
res = HTTPMethodNotAllowed()
else:
res = method(req)
except (Exception, Timeout):
self.logger.exception(_('ERROR __call__ error with %(method)s'
' %(path)s '), {'method': req.method, 'path': req.path})

View File

@ -53,7 +53,7 @@ from webob import Request, Response
from swift.common.ring import Ring
from swift.common.utils import cache_from_env, ContextPool, get_logger, \
get_remote_client, normalize_timestamp, split_path, TRUE_VALUES
get_remote_client, normalize_timestamp, split_path, TRUE_VALUES, public
from swift.common.bufferedhttp import http_connect
from swift.common.constraints import check_metadata, check_object_creation, \
check_utf8, CONTAINER_LISTING_LIMIT, MAX_ACCOUNT_NAME_LENGTH, \
@ -86,21 +86,6 @@ def update_headers(response, headers):
response.headers[name] = value
def public(func):
"""
Decorator to declare which methods are publicly accessible as HTTP
requests
:param func: function to make public
"""
func.publicly_accessible = True
@functools.wraps(func)
def wrapped(*a, **kw):
return func(*a, **kw)
return wrapped
def delay_denial(func):
"""
Decorator to declare which methods should have any swift.authorize call
@ -2022,11 +2007,8 @@ class BaseApplication(object):
self.logger.client_ip = get_remote_client(req)
try:
handler = getattr(controller, req.method)
if not getattr(handler, 'publicly_accessible'):
handler = None
getattr(handler, 'publicly_accessible')
except AttributeError:
handler = None
if not handler:
self.logger.increment('method_not_allowed')
return HTTPMethodNotAllowed(request=req)
if path_parts['version']:

View File

@ -962,6 +962,30 @@ class TestAccountController(unittest.TestCase):
self.assertEquals(errbuf.getvalue(), '')
self.assertEquals(outbuf.getvalue()[:4], '400 ')
def test_invalid_method_doesnt_exist(self):
inbuf = StringIO()
errbuf = StringIO()
outbuf = StringIO()
def start_response(*args):
outbuf.writelines(args)
self.controller.__call__({'REQUEST_METHOD': 'method_doesnt_exist',
'PATH_INFO': '/sda1/p/a'},
start_response)
self.assertEquals(errbuf.getvalue(), '')
self.assertEquals(outbuf.getvalue()[:4], '405 ')
def test_invalid_method_is_not_public(self):
inbuf = StringIO()
errbuf = StringIO()
outbuf = StringIO()
def start_response(*args):
outbuf.writelines(args)
self.controller.__call__({'REQUEST_METHOD': '__init__',
'PATH_INFO': '/sda1/p/a'},
start_response)
self.assertEquals(errbuf.getvalue(), '')
self.assertEquals(outbuf.getvalue()[:4], '405 ')
def test_params_utf8(self):
self.controller.PUT(Request.blank('/sda1/p/a',
headers={'X-Timestamp': normalize_timestamp(1)},

View File

@ -928,6 +928,30 @@ class TestContainerController(unittest.TestCase):
self.assertEquals(errbuf.getvalue(), '')
self.assertEquals(outbuf.getvalue()[:4], '400 ')
def test_invalid_method_doesnt_exist(self):
inbuf = StringIO()
errbuf = StringIO()
outbuf = StringIO()
def start_response(*args):
outbuf.writelines(args)
self.controller.__call__({'REQUEST_METHOD': 'method_doesnt_exist',
'PATH_INFO': '/sda1/p/a/c'},
start_response)
self.assertEquals(errbuf.getvalue(), '')
self.assertEquals(outbuf.getvalue()[:4], '405 ')
def test_invalid_method_is_not_public(self):
inbuf = StringIO()
errbuf = StringIO()
outbuf = StringIO()
def start_response(*args):
outbuf.writelines(args)
self.controller.__call__({'REQUEST_METHOD': '__init__',
'PATH_INFO': '/sda1/p/a/c'},
start_response)
self.assertEquals(errbuf.getvalue(), '')
self.assertEquals(outbuf.getvalue()[:4], '405 ')
def test_params_utf8(self):
self.controller.PUT(Request.blank('/sda1/p/a/c',
headers={'X-Timestamp': normalize_timestamp(1)},

View File

@ -1342,6 +1342,30 @@ class TestObjectController(unittest.TestCase):
self.assertEquals(errbuf.getvalue(), '')
self.assertEquals(outbuf.getvalue()[:4], '405 ')
def test_invalid_method_doesnt_exist(self):
inbuf = StringIO()
errbuf = StringIO()
outbuf = StringIO()
def start_response(*args):
outbuf.writelines(args)
self.object_controller.__call__({'REQUEST_METHOD': 'method_doesnt_exist',
'PATH_INFO': '/sda1/p/a/c/o'},
start_response)
self.assertEquals(errbuf.getvalue(), '')
self.assertEquals(outbuf.getvalue()[:4], '405 ')
def test_invalid_method_is_not_public(self):
inbuf = StringIO()
errbuf = StringIO()
outbuf = StringIO()
def start_response(*args):
outbuf.writelines(args)
self.object_controller.__call__({'REQUEST_METHOD': '__init__',
'PATH_INFO': '/sda1/p/a/c/o'},
start_response)
self.assertEquals(errbuf.getvalue(), '')
self.assertEquals(outbuf.getvalue()[:4], '405 ')
def test_chunked_put(self):
listener = listen(('localhost', 0))
port = listener.getsockname()[1]

View File

@ -560,6 +560,22 @@ class TestProxyServer(unittest.TestCase):
resp = app.handle_request(req)
self.assertEquals(resp.status_int, 500)
def test_internal_method_request(self):
baseapp = proxy_server.BaseApplication({},
FakeMemcache(), container_ring=FakeRing(), object_ring=FakeRing(),
account_ring=FakeRing())
resp = baseapp.handle_request(
Request.blank('/v1/a', environ={'REQUEST_METHOD': '__init__'}))
self.assertEquals(resp.status, '405 Method Not Allowed')
def test_inexistent_method_request(self):
baseapp = proxy_server.BaseApplication({},
FakeMemcache(), container_ring=FakeRing(), account_ring=FakeRing(),
object_ring=FakeRing())
resp = baseapp.handle_request(
Request.blank('/v1/a', environ={'REQUEST_METHOD': '!invalid'}))
self.assertEquals(resp.status, '405 Method Not Allowed')
def test_calls_authorize_allow(self):
called = [False]