diff --git a/apps/ops/api/playbook.py b/apps/ops/api/playbook.py index 72e62cb82..2bf52132c 100644 --- a/apps/ops/api/playbook.py +++ b/apps/ops/api/playbook.py @@ -3,6 +3,7 @@ import shutil import zipfile from django.conf import settings +from django.core.exceptions import SuspiciousFileOperation from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from rest_framework import status @@ -18,6 +19,7 @@ __all__ = ["PlaybookViewSet", "PlaybookFileBrowserAPIView"] from rest_framework.views import APIView from rest_framework.response import Response +from django.utils._os import safe_join def unzip_playbook(src, dist): @@ -37,7 +39,7 @@ class PlaybookViewSet(OrgBulkModelViewSet): raise JMSException(code='playbook_has_job', detail={"msg": _("Currently playbook is being used in a job")}) instance_id = instance.id super().perform_destroy(instance) - dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance_id.__str__()) + dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance_id.__str__()) shutil.rmtree(dest_path) def get_queryset(self): @@ -48,8 +50,8 @@ class PlaybookViewSet(OrgBulkModelViewSet): def perform_create(self, serializer): instance = serializer.save() if 'multipart/form-data' in self.request.headers['Content-Type']: - src_path = os.path.join(settings.MEDIA_ROOT, instance.path.name) - dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__()) + src_path = safe_join(settings.MEDIA_ROOT, instance.path.name) + dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__()) try: unzip_playbook(src_path, dest_path) except RuntimeError as e: @@ -60,9 +62,9 @@ class PlaybookViewSet(OrgBulkModelViewSet): else: if instance.create_method == 'blank': - dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__()) + dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__()) os.makedirs(dest_path) - with open(os.path.join(dest_path, 'main.yml'), 'w') as f: + with open(safe_join(dest_path, 'main.yml'), 'w') as f: f.write('## write your playbook here') @@ -83,13 +85,15 @@ class PlaybookFileBrowserAPIView(APIView): work_path = playbook.work_dir file_key = request.query_params.get('key', '') if file_key: - file_path = os.path.join(work_path, file_key) - with open(file_path, 'r') as f: - try: + try: + file_path = safe_join(work_path, file_key) + with open(file_path, 'r') as f: content = f.read() - except UnicodeDecodeError: - content = _('Unsupported file content') - return Response({'content': content}) + except UnicodeDecodeError: + content = _('Unsupported file content') + except SuspiciousFileOperation: + raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")}) + return Response({'content': content}) else: expand_key = request.query_params.get('expand', '') nodes = self.generate_tree(playbook, work_path, expand_key) @@ -105,7 +109,8 @@ class PlaybookFileBrowserAPIView(APIView): parent_key = '' if os.path.dirname(parent_key) == 'root': parent_key = os.path.basename(parent_key) - full_path = os.path.join(work_path, parent_key) + + full_path = safe_join(work_path, parent_key) is_directory = request.data.get('is_directory', False) content = request.data.get('content', '') @@ -117,27 +122,30 @@ class PlaybookFileBrowserAPIView(APIView): p = 'new_file.yml' else: p = 'new_dir' - np = os.path.join(full_path, p) + np = safe_join(full_path, p) n = 0 while os.path.exists(np): n += 1 - np = os.path.join(full_path, '{}({})'.format(p, n)) + np = safe_join(full_path, '{}({})'.format(p, n)) return np - if is_directory: - new_file_path = find_new_name(name) - os.makedirs(new_file_path) - else: - new_file_path = find_new_name(name, True) - with open(new_file_path, 'w') as f: - f.write(content) + try: + if is_directory: + new_file_path = find_new_name(name) + os.makedirs(new_file_path) + else: + new_file_path = find_new_name(name, True) + with open(new_file_path, 'w') as f: + f.write(content) + except SuspiciousFileOperation: + raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")}) relative_path = os.path.relpath(os.path.dirname(new_file_path), work_path) new_node = { "name": os.path.basename(new_file_path), "title": os.path.basename(new_file_path), - "id": os.path.join(relative_path, os.path.basename(new_file_path)) - if not os.path.join(relative_path, os.path.basename(new_file_path)).startswith('.') + "id": safe_join(relative_path, os.path.basename(new_file_path)) + if not safe_join(relative_path, os.path.basename(new_file_path)).startswith('.') else os.path.basename(new_file_path), "isParent": is_directory, "pId": relative_path if not relative_path.startswith('.') else 'root', @@ -156,7 +164,7 @@ class PlaybookFileBrowserAPIView(APIView): new_name = request.data.get('new_name', '') if file_key in self.protected_files and new_name: - return Response({'msg': '{} can not be rename'.format(file_key)}, status=status.HTTP_400_BAD_REQUEST) + raise JMSException(code='this_file_can_not_be_rename', detail={"msg": _("This file can not be rename")}) if os.path.dirname(file_key) == 'root': file_key = os.path.basename(file_key) @@ -166,16 +174,20 @@ class PlaybookFileBrowserAPIView(APIView): if not file_key or file_key == 'root': return Response(status=status.HTTP_400_BAD_REQUEST) - file_path = os.path.join(work_path, file_key) + file_path = safe_join(work_path, file_key) # rename if new_name: - new_file_path = os.path.join(os.path.dirname(file_path), new_name) - if new_file_path == file_path: - return Response(status=status.HTTP_200_OK) - if os.path.exists(new_file_path): - return Response({'msg': '{} already exists'.format(new_name)}, status=status.HTTP_400_BAD_REQUEST) - os.rename(file_path, new_file_path) + try: + new_file_path = safe_join(os.path.dirname(file_path), new_name) + if new_file_path == file_path: + return Response(status=status.HTTP_200_OK) + if os.path.exists(new_file_path): + raise JMSException(code='file_already exists', detail={"msg": _("File already exists")}) + os.rename(file_path, new_file_path) + except SuspiciousFileOperation: + raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")}) + # edit content else: if not is_directory: @@ -189,10 +201,11 @@ class PlaybookFileBrowserAPIView(APIView): work_path = playbook.work_dir file_key = request.query_params.get('key', '') if not file_key: - return Response({'msg': 'key is required'}, status=status.HTTP_400_BAD_REQUEST) + raise JMSException(code='file_key_is_required', detail={"msg": _("File key is required")}) + if file_key in self.protected_files: - return Response({'msg': ' {} can not be delete'.format(file_key)}, status=status.HTTP_400_BAD_REQUEST) - file_path = os.path.join(work_path, file_key) + raise JMSException(code='This file can not be delete', detail={"msg": _("This file can not be delete")}) + file_path = safe_join(work_path, file_key) if os.path.isdir(file_path): shutil.rmtree(file_path) else: @@ -216,11 +229,12 @@ class PlaybookFileBrowserAPIView(APIView): relative_path = os.path.relpath(path, root_path) for d in dirs: + dir_id = os.path.relpath(safe_join(path, d), root_path) + node = { "name": d, "title": d, - "id": os.path.join(relative_path, d) if not os.path.join(relative_path, d).startswith( - '.') else d, + "id": dir_id, "isParent": True, "open": True, "pId": relative_path if not relative_path.startswith('.') else 'root', @@ -230,12 +244,12 @@ class PlaybookFileBrowserAPIView(APIView): node['open'] = True nodes.append(node) for f in files: + file_id = os.path.relpath(safe_join(path, f), root_path) node = { "name": f, "title": f, "iconSkin": 'file', - "id": os.path.join(relative_path, f) if not os.path.join(relative_path, f).startswith( - '.') else f, + "id": file_id, "isParent": False, "open": False, "pId": relative_path if not relative_path.startswith('.') else 'root',