From a1d1c3ba2eb5657035d8db9d67c62a0894d79c69 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Tue, 23 Jun 2026 00:27:52 +0200 Subject: [PATCH] fix(13): CR-02 apply PurePosixPath.name guard in WebDAV upload_file and rename to prevent path traversal --- backend/storage/webdav_backend.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/backend/storage/webdav_backend.py b/backend/storage/webdav_backend.py index c36a8a6..a878889 100644 --- a/backend/storage/webdav_backend.py +++ b/backend/storage/webdav_backend.py @@ -41,7 +41,7 @@ import io import uuid import urllib.parse from datetime import datetime, timezone -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import Optional from webdav3.client import Client @@ -505,13 +505,20 @@ class WebDAVBackend(StorageBackend, MutableCloudResourceAdapter): {'kind': 'updated', 'reason': 'renamed', 'provider_item_id': str, 'name': str} """ - # Compute new path: same parent directory, new name + # T-13-04 path-traversal guard: strip all directory components from the + # caller-supplied new_name. A value like "../../evil" would otherwise + # escape the item's parent directory when the new path is computed. + safe_new_name = PurePosixPath(new_name).name + if not safe_new_name or safe_new_name in (".", ".."): + safe_new_name = new_name # caller-validated; keep original on empty result + + # Compute new path: same parent directory, safe new name parts = provider_item_id.rstrip("/").rsplit("/", 1) if len(parts) == 2: parent_path = parts[0] - new_path = f"{parent_path}/{new_name}" + new_path = f"{parent_path}/{safe_new_name}" else: - new_path = new_name + new_path = safe_new_name def _move_rename() -> dict: try: @@ -520,7 +527,7 @@ class WebDAVBackend(StorageBackend, MutableCloudResourceAdapter): "kind": MUT_KIND_UPDATED, "reason": MUT_REASON_RENAMED, "provider_item_id": new_path, - "name": new_name, + "name": safe_new_name, } except Exception as exc: return self._normalize_error(exc) @@ -627,10 +634,18 @@ class WebDAVBackend(StorageBackend, MutableCloudResourceAdapter): {'kind': 'uploaded', 'reason': 'created', 'provider_item_id': str, 'name': str, 'parent_ref': str | None, 'size': int} """ + # T-13-04 path-traversal guard: strip all directory components from the + # caller-supplied filename so a value like "../../evil" cannot escape + # the intended parent_ref. PurePosixPath.name returns only the final + # segment. Fall back to "upload" for empty or dot-only results. + safe_filename = PurePosixPath(filename).name + if not safe_filename or safe_filename in (".", ".."): + safe_filename = "upload" + if parent_ref: - object_path = f"{parent_ref.rstrip('/')}/{filename}" + object_path = f"{parent_ref.rstrip('/')}/{safe_filename}" else: - object_path = filename + object_path = safe_filename def _upload() -> dict: try: @@ -640,7 +655,7 @@ class WebDAVBackend(StorageBackend, MutableCloudResourceAdapter): "kind": MUT_KIND_UPLOADED, "reason": MUT_REASON_CREATED, "provider_item_id": object_path, - "name": filename, + "name": safe_filename, "parent_ref": parent_ref, "size": len(content), }