Skip to content

Commit c34b92a

Browse files
fix: address PR review feedback on filename sanitization
- Remove unused `import re` - Normalize backslashes to forward slashes before os.path.basename() so Windows-style paths (e.g. ..\..\..\evil.txt) are handled on POSIX - Strip whitespace before removing leading dots so " .env" is caught - Return None instead of "unnamed" for empty input so existing `if not name` validation guards remain effective - Add `or "unnamed"` fallback at call sites that lack a name guard
1 parent dc29407 commit c34b92a

7 files changed

Lines changed: 22 additions & 14 deletions

File tree

apps/api/plane/api/views/asset.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def post(self, request):
115115
This endpoint generates the necessary credentials for direct S3 upload.
116116
"""
117117
# get the asset key
118-
name = sanitize_filename(request.data.get("name"))
118+
name = sanitize_filename(request.data.get("name")) or "unnamed"
119119
type = request.data.get("type", "image/jpeg")
120120
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
121121
entity_type = request.data.get("entity_type", False)
@@ -288,7 +288,7 @@ def post(self, request):
288288
necessary credentials for direct S3 upload with server-side authentication.
289289
"""
290290
# get the asset key
291-
name = sanitize_filename(request.data.get("name"))
291+
name = sanitize_filename(request.data.get("name")) or "unnamed"
292292
type = request.data.get("type", "image/jpeg")
293293
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
294294
entity_type = request.data.get("entity_type", False)

apps/api/plane/app/views/asset/v2.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def entity_asset_delete(self, entity_type, asset, request):
109109

110110
def post(self, request):
111111
# get the asset key
112-
name = sanitize_filename(request.data.get("name"))
112+
name = sanitize_filename(request.data.get("name")) or "unnamed"
113113
type = request.data.get("type", "image/jpeg")
114114
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
115115
entity_type = request.data.get("entity_type", False)
@@ -313,7 +313,7 @@ def entity_asset_delete(self, entity_type, asset, request):
313313
return
314314

315315
def post(self, request, slug):
316-
name = sanitize_filename(request.data.get("name"))
316+
name = sanitize_filename(request.data.get("name")) or "unnamed"
317317
type = request.data.get("type", "image/jpeg")
318318
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
319319
entity_type = request.data.get("entity_type")
@@ -512,7 +512,7 @@ def get_entity_id_field(self, entity_type, entity_id):
512512

513513
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
514514
def post(self, request, slug, project_id):
515-
name = sanitize_filename(request.data.get("name"))
515+
name = sanitize_filename(request.data.get("name")) or "unnamed"
516516
type = request.data.get("type", "image/jpeg")
517517
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
518518
entity_type = request.data.get("entity_type", "")
@@ -758,7 +758,7 @@ def post(self, request, slug, asset_id):
758758
if not original_asset:
759759
return Response({"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND)
760760

761-
destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{sanitize_filename(original_asset.attributes.get('name'))}"
761+
destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{sanitize_filename(original_asset.attributes.get('name')) or 'unnamed'}"
762762
duplicated_asset = FileAsset.objects.create(
763763
attributes={
764764
"name": original_asset.attributes.get("name"),

apps/api/plane/app/views/issue/attachment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
9898

9999
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
100100
def post(self, request, slug, project_id, issue_id):
101-
name = sanitize_filename(request.data.get("name"))
101+
name = sanitize_filename(request.data.get("name")) or "unnamed"
102102
type = request.data.get("type", False)
103103
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
104104

apps/api/plane/db/models/asset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818

1919
def get_upload_path(instance, filename):
20-
filename = sanitize_filename(filename)
20+
filename = sanitize_filename(filename) or "unnamed"
2121
if instance.workspace_id is not None:
2222
return f"{instance.workspace.id}/{uuid4().hex}-{filename}"
2323
return f"user-{uuid4().hex}-{filename}"

apps/api/plane/db/models/issue.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ def __str__(self):
377377

378378

379379
def get_upload_path(instance, filename):
380-
filename = sanitize_filename(filename)
380+
filename = sanitize_filename(filename) or "unnamed"
381381
return f"{instance.workspace.id}/{uuid4().hex}-{filename}"
382382

383383

apps/api/plane/space/views/asset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def post(self, request, anchor):
7474
return Response({"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND)
7575

7676
# Get the asset
77-
name = sanitize_filename(request.data.get("name"))
77+
name = sanitize_filename(request.data.get("name")) or "unnamed"
7878
type = request.data.get("type", "image/jpeg")
7979
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
8080
entity_type = request.data.get("entity_type", "")

apps/api/plane/utils/path_validator.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
# Python imports
1010
import os
11-
import re
1211
from urllib.parse import urlparse
1312

1413

@@ -18,27 +17,36 @@ def sanitize_filename(filename):
1817
1918
Strips directory components, path traversal sequences, and null bytes
2019
from user-supplied filenames used in upload paths and S3 object keys.
20+
21+
Returns None for empty/missing input so callers can still validate
22+
that a filename was provided.
2123
"""
2224
if not filename or not isinstance(filename, str):
23-
return "unnamed"
25+
return None
2426

2527
# Strip null bytes
2628
filename = filename.replace("\x00", "")
2729

30+
# Normalize backslashes so os.path.basename handles Windows-style paths on POSIX
31+
filename = filename.replace("\\", "/")
32+
2833
# Take only the basename to remove any directory components
2934
filename = os.path.basename(filename)
3035

3136
# Remove any remaining path traversal sequences
3237
filename = filename.replace("..", "")
3338

39+
# Strip whitespace before removing leading dots so " .env" is caught
40+
filename = filename.strip()
41+
3442
# Remove leading dots (hidden files)
3543
filename = filename.lstrip(".")
3644

37-
# Strip whitespace
45+
# Strip any remaining whitespace
3846
filename = filename.strip()
3947

4048
if not filename:
41-
return "unnamed"
49+
return None
4250

4351
return filename
4452

0 commit comments

Comments
 (0)