fix(13): CR-02 apply PurePosixPath.name guard in WebDAV upload_file and rename to prevent path traversal

This commit is contained in:
curo1305
2026-06-23 00:27:52 +02:00
parent d4b2697109
commit a1d1c3ba2e
+23 -8
View File
@@ -41,7 +41,7 @@ import io
import uuid import uuid
import urllib.parse import urllib.parse
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path, PurePosixPath
from typing import Optional from typing import Optional
from webdav3.client import Client from webdav3.client import Client
@@ -505,13 +505,20 @@ class WebDAVBackend(StorageBackend, MutableCloudResourceAdapter):
{'kind': 'updated', 'reason': 'renamed', 'provider_item_id': str, 'name': str} {'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) parts = provider_item_id.rstrip("/").rsplit("/", 1)
if len(parts) == 2: if len(parts) == 2:
parent_path = parts[0] parent_path = parts[0]
new_path = f"{parent_path}/{new_name}" new_path = f"{parent_path}/{safe_new_name}"
else: else:
new_path = new_name new_path = safe_new_name
def _move_rename() -> dict: def _move_rename() -> dict:
try: try:
@@ -520,7 +527,7 @@ class WebDAVBackend(StorageBackend, MutableCloudResourceAdapter):
"kind": MUT_KIND_UPDATED, "kind": MUT_KIND_UPDATED,
"reason": MUT_REASON_RENAMED, "reason": MUT_REASON_RENAMED,
"provider_item_id": new_path, "provider_item_id": new_path,
"name": new_name, "name": safe_new_name,
} }
except Exception as exc: except Exception as exc:
return self._normalize_error(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} {'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: if parent_ref:
object_path = f"{parent_ref.rstrip('/')}/{filename}" object_path = f"{parent_ref.rstrip('/')}/{safe_filename}"
else: else:
object_path = filename object_path = safe_filename
def _upload() -> dict: def _upload() -> dict:
try: try:
@@ -640,7 +655,7 @@ class WebDAVBackend(StorageBackend, MutableCloudResourceAdapter):
"kind": MUT_KIND_UPLOADED, "kind": MUT_KIND_UPLOADED,
"reason": MUT_REASON_CREATED, "reason": MUT_REASON_CREATED,
"provider_item_id": object_path, "provider_item_id": object_path,
"name": filename, "name": safe_filename,
"parent_ref": parent_ref, "parent_ref": parent_ref,
"size": len(content), "size": len(content),
} }