From 7c04e01cde2f308eff587ad02f9b01ca3b15202c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 25 Mar 2016 18:23:01 +0100 Subject: [PATCH] Some security fixes and other fixes for file storage. Also added unittests for creating files. --- pillar/application/modules/file_storage.py | 49 +++++++++++------ pillar/application/utils/__init__.py | 21 ++++++++ pillar/application/utils/storage.py | 2 +- tests/BlenderDesktopLogo.png | Bin 0 -> 32969 bytes tests/common_test_class.py | 16 +++++- tests/test_file_storage.py | 58 +++++++++++++++++++++ 6 files changed, 127 insertions(+), 19 deletions(-) create mode 100644 tests/BlenderDesktopLogo.png create mode 100644 tests/test_file_storage.py diff --git a/pillar/application/modules/file_storage.py b/pillar/application/modules/file_storage.py index 76dc4de8..07ebe648 100644 --- a/pillar/application/modules/file_storage.py +++ b/pillar/application/modules/file_storage.py @@ -8,12 +8,14 @@ import eve.utils from bson import ObjectId from eve.methods.patch import patch_internal from eve.methods.put import put_internal -from flask import Blueprint +from flask import Blueprint, safe_join from flask import jsonify from flask import request +from flask import abort from flask import send_from_directory from flask import url_for, helpers +from application import utils from application.utils import remove_private_keys from application.utils.cdn import hash_file_path from application.utils.encoding import Encoder @@ -75,8 +77,8 @@ def build_thumbnails(file_path=None, file_id=None): file_ = files_collection.find_one({"_id": ObjectId(file_id)}) file_path = file_['name'] - file_full_path = os.path.join(app.config['SHARED_DIR'], file_path[:2], - file_path) + file_full_path = safe_join(safe_join(app.config['SHARED_DIR'], file_path[:2]), + file_path) # Does the original file exist? if not os.path.isfile(file_full_path): return "", 404 @@ -147,26 +149,29 @@ def index(file_name=None): return jsonify({'url': url_for('file_storage.index', file_name=file_name)}) -def process_file(src_file): - """Process the file +def process_file(file_id, src_file): + """Process the file. + + :param file_id: '_id' key of the file + :param src_file: POSTed data of the file, lacks private properties. """ from application import app - file_id = src_file['_id'] - # Remove properties that do not belong in the collection - internal_fields = ['_id', '_etag', '_updated', '_created', '_status'] - for field in internal_fields: - src_file.pop(field, None) + src_file = utils.remove_private_keys(src_file) - files_collection = app.data.driver.db['files'] - file_abs_path = os.path.join( - app.config['SHARED_DIR'], src_file['name'][:2], src_file['name']) + filename = src_file['name'] + file_abs_path = safe_join(safe_join(app.config['SHARED_DIR'], filename[:2]), filename) + + if not os.path.exists(file_abs_path): + log.warning("POSTed file document %r refers to non-existant file on file system %s!", + file_id, file_abs_path) + abort(422, "POSTed file document refers to non-existant file on file system!") src_file['length'] = os.stat(file_abs_path).st_size content_type = src_file['content_type'].split('/') src_file['format'] = content_type[1] mime_type = content_type[0] - src_file['file_path'] = src_file['name'] + src_file['file_path'] = filename if mime_type == 'image': from PIL import Image @@ -197,7 +202,7 @@ def process_file(src_file): src_file['variations'] = [] # Create variations for v in variations: - root, ext = os.path.splitext(src_file['name']) + root, ext = os.path.splitext(filename) filename = "{0}-{1}p.{2}".format(root, res_y, v) video_duration = None if src_video_data['duration']: @@ -250,6 +255,9 @@ def process_file(src_file): p = Process(target=encode, args=(file_abs_path, src_file, res_y)) p.start() + else: + log.info("POSTed file was of type %r, which isn't thumbnailed/encoded.", mime_type) + if mime_type != 'video': # Sync the whole subdir sync_path = os.path.split(file_abs_path)[0] @@ -259,7 +267,7 @@ def process_file(src_file): p.start() # Update the original file with additional info, e.g. image resolution - r = put_internal('files', src_file, **{'_id': ObjectId(file_id)}) + put_internal('files', src_file, _id=ObjectId(file_id)) def delete_file(file_item): @@ -383,7 +391,14 @@ def post_POST_files(request, payload): """After an file object has been created, we do the necessary processing and further update it. """ - process_file(request.get_json()) + + if 200 <= payload.status_code < 300: + import json + posted_properties = json.loads(request.data) + private_properties = json.loads(payload.data) + file_id = private_properties['_id'] + + process_file(file_id, posted_properties) def before_deleting_file(item): diff --git a/pillar/application/utils/__init__.py b/pillar/application/utils/__init__.py index b15edcb2..c56c455e 100644 --- a/pillar/application/utils/__init__.py +++ b/pillar/application/utils/__init__.py @@ -1,4 +1,9 @@ import copy +import json +import datetime + +import bson +from eve import RFC1123_DATE_FORMAT def remove_private_keys(document): @@ -11,3 +16,19 @@ def remove_private_keys(document): del patch_info[key] return patch_info + + +class PillarJSONEncoder(json.JSONEncoder): + """JSON encoder with support for Pillar resources.""" + + def default(self, obj): + if isinstance(obj, datetime.datetime): + if obj.tzinfo is None: + raise ValueError('All datetime.datetime objects should be timezone-aware.') + return obj.strftime(RFC1123_DATE_FORMAT) + + if isinstance(obj, bson.ObjectId): + return str(obj) + + # Let the base class default method raise the TypeError + return json.JSONEncoder.default(self, obj) diff --git a/pillar/application/utils/storage.py b/pillar/application/utils/storage.py index 854f086c..218729ef 100644 --- a/pillar/application/utils/storage.py +++ b/pillar/application/utils/storage.py @@ -70,7 +70,7 @@ def push_to_storage(project_id, full_path, backend='cgs'): # XXX Make public on the fly if it's an image and small preview. # This should happen by reading the database (push to storage # should change to accomodate it). - if full_path.endswith('-t.jpg'): + if blob is not None and full_path.endswith('-t.jpg'): blob.make_public() os.remove(full_path) diff --git a/tests/BlenderDesktopLogo.png b/tests/BlenderDesktopLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..fd976d7aa1af0b24e61e4f4c37d418b031cd6726 GIT binary patch literal 32969 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 OKf}9bx|P}p^#20}ydJ0k literal 0 HcmV?d00001 diff --git a/tests/common_test_class.py b/tests/common_test_class.py index 45263ec8..b52f35d7 100644 --- a/tests/common_test_class.py +++ b/tests/common_test_class.py @@ -34,6 +34,7 @@ class AbstractPillarTest(TestMinimal): app.config['BLENDER_ID_ENDPOINT'] = BLENDER_ID_ENDPOINT logging.getLogger('application').setLevel(logging.DEBUG) logging.getLogger('werkzeug').setLevel(logging.DEBUG) + logging.getLogger('eve').setLevel(logging.DEBUG) self.app = app self.client = app.test_client() @@ -59,7 +60,20 @@ class AbstractPillarTest(TestMinimal): projects_collection.insert_one(EXAMPLE_PROJECT) result = files_collection.insert_one(file) file_id = result.inserted_id - return file_id, EXAMPLE_FILE + return file_id, file + + def ensure_project_exists(self, project_overrides=None): + with self.app.test_request_context(): + projects_collection = self.app.data.driver.db['projects'] + assert isinstance(projects_collection, pymongo.collection.Collection) + + project = copy.deepcopy(EXAMPLE_PROJECT) + if project_overrides is not None: + project.update(project_overrides) + + result = projects_collection.insert_one(project) + project_id = result.inserted_id + return project_id, project def htp_blenderid_validate_unhappy(self): """Sets up HTTPretty to mock unhappy validation flow.""" diff --git a/tests/test_file_storage.py b/tests/test_file_storage.py new file mode 100644 index 00000000..b0c64b3d --- /dev/null +++ b/tests/test_file_storage.py @@ -0,0 +1,58 @@ +"""Test cases for file handling.""" + +from __future__ import print_function + +import os +import shutil +import copy +import json + +from common_test_class import AbstractPillarTest, MY_PATH +from common_test_data import EXAMPLE_FILE + + +class FileUploadingTest(AbstractPillarTest): + + def test_create_file_missing_on_fs(self): + from application import utils + from application.utils import PillarJSONEncoder + + to_post = utils.remove_private_keys(EXAMPLE_FILE) + json_file = json.dumps(to_post, cls=PillarJSONEncoder) + + with self.app.test_request_context(): + self.ensure_project_exists() + + resp = self.client.post('/files', + data=json_file, + headers={'Content-Type': 'application/json'}) + + self.assertEqual(422, resp.status_code) + + + def test_create_file_exists_on_fs(self): + from application import utils + from application.utils import PillarJSONEncoder + + filename = 'BlenderDesktopLogo.png' + full_file = copy.deepcopy(EXAMPLE_FILE) + full_file[u'name'] = filename + to_post = utils.remove_private_keys(full_file) + json_file = json.dumps(to_post, cls=PillarJSONEncoder) + + with self.app.test_request_context(): + self.ensure_project_exists() + + target_dir = os.path.join(self.app.config['SHARED_DIR'], filename[:2]) + if os.path.exists(target_dir): + assert os.path.isdir(target_dir) + else: + os.makedirs(target_dir) + shutil.copy(os.path.join(MY_PATH, filename), target_dir) + + resp = self.client.post('/files', + data=json_file, + headers={'Content-Type': 'application/json'}) + + self.assertEqual(201, resp.status_code) +