Fix object copy with empty source

When s3 client make a copy request with empty copy source,
current swift3 will return a 500 error. This is caused from
a lack of header validation.

This fixes the behavior to make a 400 Bad Request (InvalidArgument).

Change-Id: I60c7e480582f5b9258433306bbf228b7291e6236
This commit is contained in:
Kota Tsuyuzaki
2014-12-08 23:40:26 -08:00
parent 3c40629f48
commit c330da7081
5 changed files with 81 additions and 14 deletions

View File

@@ -58,14 +58,13 @@ class ObjectController(Controller):
Handle PUT Object and PUT Object (Copy) request Handle PUT Object and PUT Object (Copy) request
""" """
if CONF.s3_acl: if CONF.s3_acl:
if 'HTTP_X_AMZ_COPY_SOURCE' in req.environ: if 'X-Amz-Copy-Source' in req.headers:
src_path = req.environ['HTTP_X_AMZ_COPY_SOURCE'] src_path = req.headers['X-Amz-Copy-Source']
src_path = src_path if src_path[0] == '/' else ('/' + src_path) src_path = src_path if src_path.startswith('/') else \
('/' + src_path)
src_bucket, src_obj = split_path(src_path, 0, 2, True) src_bucket, src_obj = split_path(src_path, 0, 2, True)
req.get_response(self.app, 'HEAD', src_bucket, src_obj, req.get_response(self.app, 'HEAD', src_bucket, src_obj,
permission='READ') permission='READ')
b_resp = req.get_response(self.app, 'HEAD', obj='') b_resp = req.get_response(self.app, 'HEAD', obj='')
# To avoid overwriting the existing object by unauthorized user, # To avoid overwriting the existing object by unauthorized user,
# we send HEAD request first before writing the object to make # we send HEAD request first before writing the object to make
@@ -83,11 +82,11 @@ class ObjectController(Controller):
resp = req.get_response(self.app) resp = req.get_response(self.app)
if 'HTTP_X_COPY_FROM' in req.environ: if 'X-Amz-Copy-Source' in req.headers:
elem = Element('CopyObjectResult') elem = Element('CopyObjectResult')
SubElement(elem, 'ETag').text = '"%s"' % resp.etag SubElement(elem, 'ETag').text = '"%s"' % resp.etag
body = tostring(elem, use_s3ns=False) body = tostring(elem, use_s3ns=False)
return HTTPOk(body=body) return HTTPOk(body=body, headers=resp.headers)
resp.status = HTTP_OK resp.status = HTTP_OK

View File

@@ -28,7 +28,6 @@ from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \
HTTP_PARTIAL_CONTENT, HTTP_NOT_MODIFIED, HTTP_PRECONDITION_FAILED, \ HTTP_PARTIAL_CONTENT, HTTP_NOT_MODIFIED, HTTP_PRECONDITION_FAILED, \
HTTP_REQUESTED_RANGE_NOT_SATISFIABLE, HTTP_LENGTH_REQUIRED, \ HTTP_REQUESTED_RANGE_NOT_SATISFIABLE, HTTP_LENGTH_REQUIRED, \
HTTP_BAD_REQUEST HTTP_BAD_REQUEST
from swift.common.constraints import check_utf8 from swift.common.constraints import check_utf8
from swift3.controllers import ServiceController, BucketController, \ from swift3.controllers import ServiceController, BucketController, \
@@ -43,7 +42,7 @@ from swift3.response import AccessDenied, InvalidArgument, InvalidDigest, \
MissingContentLength, InvalidStorageClass, S3NotImplemented, InvalidURI, \ MissingContentLength, InvalidStorageClass, S3NotImplemented, InvalidURI, \
MalformedXML, InvalidRequest MalformedXML, InvalidRequest
from swift3.exception import NotS3Request, BadSwiftRequest from swift3.exception import NotS3Request, BadSwiftRequest
from swift3.utils import utf8encode, LOGGER from swift3.utils import utf8encode, LOGGER, check_path_header
from swift3.cfg import CONF from swift3.cfg import CONF
from swift3.subresource import decode_acl, encode_acl from swift3.subresource import decode_acl, encode_acl
from swift3.utils import sysmeta_header from swift3.utils import sysmeta_header
@@ -215,6 +214,16 @@ class Request(swob.Request):
except Exception: except Exception:
raise InvalidDigest(content_md5=value) raise InvalidDigest(content_md5=value)
if 'X-Amz-Copy-Source' in self.headers:
try:
check_path_header(self, 'X-Amz-Copy-Source', 2, '')
except swob.HTTPException:
msg = 'Copy Source must mention the source bucket and key: ' \
'sourcebucket/sourcekey'
raise InvalidArgument('x-amz-copy-source',
self.headers['X-Amz-Copy-Source'],
msg)
if 'x-amz-metadata-directive' in self.headers: if 'x-amz-metadata-directive' in self.headers:
value = self.headers['x-amz-metadata-directive'] value = self.headers['x-amz-metadata-directive']
if value not in ('COPY', 'REPLACE'): if value not in ('COPY', 'REPLACE'):

View File

@@ -16,6 +16,7 @@
import unittest import unittest
from datetime import datetime from datetime import datetime
import hashlib import hashlib
from os.path import join
from swift.common import swob from swift.common import swob
from swift.common.swob import Request from swift.common.swob import Request
@@ -205,6 +206,22 @@ class TestSwift3Obj(Swift3TestCase):
code = self._test_method_error('PUT', '/bucket/object', code = self._test_method_error('PUT', '/bucket/object',
swob.HTTPServiceUnavailable) swob.HTTPServiceUnavailable)
self.assertEquals(code, 'InternalError') self.assertEquals(code, 'InternalError')
code = self._test_method_error('PUT', '/bucket/object',
swob.HTTPCreated,
{'X-Amz-Copy-Source': ''})
self.assertEquals(code, 'InvalidArgument')
code = self._test_method_error('PUT', '/bucket/object',
swob.HTTPCreated,
{'X-Amz-Copy-Source': '/'})
self.assertEquals(code, 'InvalidArgument')
code = self._test_method_error('PUT', '/bucket/object',
swob.HTTPCreated,
{'X-Amz-Copy-Source': '/bucket'})
self.assertEquals(code, 'InvalidArgument')
code = self._test_method_error('PUT', '/bucket/object',
swob.HTTPCreated,
{'X-Amz-Copy-Source': '/bucket/'})
self.assertEquals(code, 'InvalidArgument')
@s3acl @s3acl
def test_object_PUT(self): def test_object_PUT(self):
@@ -357,20 +374,22 @@ class TestSwift3Obj(Swift3TestCase):
self._test_object_for_s3acl('DELETE', 'test:full_control') self._test_object_for_s3acl('DELETE', 'test:full_control')
self.assertEquals(status.split()[0], '204') self.assertEquals(status.split()[0], '204')
def _test_object_copy_for_s3acl(self, account, src_permission=None): def _test_object_copy_for_s3acl(self, account, src_permission=None,
src_path='/src_bucket/src_obj'):
owner = 'test:tester' owner = 'test:tester'
grants = [Grant(User(account), src_permission)] \ grants = [Grant(User(account), src_permission)] \
if src_permission else [Grant(User(owner), 'FULL_CONTROL')] if src_permission else [Grant(User(owner), 'FULL_CONTROL')]
src_o_headers = \ src_o_headers = \
encode_acl('object', ACL(Owner(owner, owner), grants)) encode_acl('object', ACL(Owner(owner, owner), grants))
self.swift.register('HEAD', '/v1/AUTH_test/src_bucket/src_obj', self.swift.register(
swob.HTTPOk, src_o_headers, None) 'HEAD', join('/v1/AUTH_test', src_path.lstrip('/')),
swob.HTTPOk, src_o_headers, None)
req = Request.blank( req = Request.blank(
'/bucket/object', '/bucket/object',
environ={'REQUEST_METHOD': 'PUT'}, environ={'REQUEST_METHOD': 'PUT'},
headers={'Authorization': 'AWS %s:hmac' % account, headers={'Authorization': 'AWS %s:hmac' % account,
'X-Amz-Copy-Source': '/src_bucket/src_obj'}) 'X-Amz-Copy-Source': src_path})
return self.call_swift3(req) return self.call_swift3(req)
@@ -417,5 +436,14 @@ class TestSwift3Obj(Swift3TestCase):
self._test_object_copy_for_s3acl(account, 'READ') self._test_object_copy_for_s3acl(account, 'READ')
self.assertEquals(status.split()[0], '403') self.assertEquals(status.split()[0], '403')
@s3acl(s3acl_only=True)
def test_object_PUT_copy_empty_src_path(self):
self.swift.register('PUT', '/v1/AUTH_test/bucket/object',
swob.HTTPPreconditionFailed, {}, None)
status, headers, body = self._test_object_copy_for_s3acl(
'test:write', 'READ', src_path='')
self.assertEquals(status.split()[0], '400')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -491,6 +491,5 @@ class TestSwift3S3Acl(Swift3TestCase):
self.assertRaises(TypeError, fake_class.s3acl_s3only_error) self.assertRaises(TypeError, fake_class.s3acl_s3only_error)
self.assertEquals(None, fake_class.s3acl_s3only_no_error()) self.assertEquals(None, fake_class.s3acl_s3only_no_error())
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -20,6 +20,11 @@ import base64
from swift.common.utils import get_logger from swift.common.utils import get_logger
# Need for check_path_header
from swift.common import utils
from swift.common.swob import HTTPPreconditionFailed
from urllib import unquote
from swift3.cfg import CONF from swift3.cfg import CONF
LOGGER = get_logger(CONF, log_route='swift3') LOGGER = get_logger(CONF, log_route='swift3')
@@ -64,3 +69,30 @@ def utf8decode(s):
if isinstance(s, str): if isinstance(s, str):
s = s.decode('utf8') s = s.decode('utf8')
return s return s
def check_path_header(req, name, length, error_msg):
# FIXME: replace swift.common.constraints check_path_header
# when swift3 supports swift 2.2 or later
"""
Validate that the value of path-like header is
well formatted. We assume the caller ensures that
specific header is present in req.headers.
:param req: HTTP request object
:param name: header name
:param length: length of path segment check
:param error_msg: error message for client
:returns: A tuple with path parts according to length
:raise: HTTPPreconditionFailed if header value
is not well formatted.
"""
src_header = unquote(req.headers.get(name))
if not src_header.startswith('/'):
src_header = '/' + src_header
try:
return utils.split_path(src_header, length, length, True)
except ValueError:
raise HTTPPreconditionFailed(
request=req,
body=error_msg)