Skip to content

fix: sanitize filenames in upload paths to prevent path traversal#8879

Open
sriramveeraghanta wants to merge 4 commits intopreviewfrom
fix/sanitize-upload-filenames
Open

fix: sanitize filenames in upload paths to prevent path traversal#8879
sriramveeraghanta wants to merge 4 commits intopreviewfrom
fix/sanitize-upload-filenames

Conversation

@sriramveeraghanta
Copy link
Copy Markdown
Member

@sriramveeraghanta sriramveeraghanta commented Apr 9, 2026

Summary

  • Adds server-side sanitize_filename() utility that strips path traversal sequences (../), null bytes, directory components, and leading dots from user-supplied filenames
  • Applies sanitization across all file upload endpoints (10 locations) and both get_upload_path() model functions
  • Addresses GHSA-v57h-5999-w7xp (path traversal in file upload)

Context

A security advisory reported that user-supplied filenames flow unsanitized into S3 object keys via asset_key construction and get_upload_path(). While S3 keys are flat strings (no filesystem traversal), this fix adds defense-in-depth against S3 key pollution and protects against risk if the storage backend were ever changed.

Files changed:

  • apps/api/plane/utils/path_validator.py — new sanitize_filename() function
  • apps/api/plane/db/models/asset.py — sanitize in get_upload_path()
  • apps/api/plane/db/models/issue.py — sanitize in get_upload_path()
  • apps/api/plane/app/views/asset/v2.py — sanitize in 3 upload endpoints + duplicate endpoint
  • apps/api/plane/app/views/issue/attachment.py — sanitize in attachment upload
  • apps/api/plane/api/views/asset.py — sanitize in 3 API upload endpoints
  • apps/api/plane/api/views/issue.py — sanitize in issue attachment API
  • apps/api/plane/space/views/asset.py — sanitize in space entity upload

Test plan

  • Verify normal file uploads still work (images, attachments)
  • Verify filenames with ../ sequences are stripped to basename
  • Verify null/empty filenames fall back to "unnamed"
  • Verify hidden files (starting with .) have leading dots stripped

Summary by CodeRabbit

  • Bug Fixes
    • Filenames for uploaded assets and issue attachments are now sanitized to remove invalid characters, null bytes, and path-traversal sequences.
    • Sanitized names are used when generating storage paths and presigned uploads; empty or invalid names fall back to "unnamed".
    • Duplication now uses sanitized destination names to ensure consistent storage keys.

…SA-v57h-5999-w7xp)

Add server-side filename sanitization across all file upload endpoints
to prevent path traversal sequences (../) in user-supplied filenames
from being incorporated into S3 object keys. While S3 keys are flat
strings and not vulnerable to filesystem traversal, this adds
defense-in-depth and prevents S3 key pollution.

Changes:
- Add sanitize_filename() utility in path_validator.py
- Sanitize filenames in get_upload_path() for FileAsset and IssueAttachment models
- Sanitize name parameter in all upload view endpoints
Copilot AI review requested due to automatic review settings April 9, 2026 17:56
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a7ea318a-9deb-4387-8d52-a9b0872cf006

📥 Commits

Reviewing files that changed from the base of the PR and between 8f01000 and edc588b.

📒 Files selected for processing (1)
  • apps/api/plane/app/views/asset/v2.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/plane/app/views/asset/v2.py

📝 Walkthrough

Walkthrough

Adds sanitize_filename and applies it across asset and issue upload flows (views and model upload path builders), causing filenames to be normalized and introducing "unnamed"/UUID fallbacks in places where sanitized results are falsy.

Changes

Cohort / File(s) Summary
Sanitization utility
apps/api/plane/utils/path_validator.py
Added sanitize_filename(filename) that normalizes input (removes null bytes, normalizes slashes, strips path traversal and leading dots) and returns None for invalid inputs.
Model upload path builders
apps/api/plane/db/models/asset.py, apps/api/plane/db/models/issue.py
get_upload_path(...) now sanitizes filename and falls back to uuid4().hex when sanitization yields a falsy value, changing the stored path filename component.
API view endpoints (plane/api & app/v2)
apps/api/plane/api/views/asset.py, apps/api/plane/app/views/asset/v2.py
Applied sanitize_filename() to name from request.data before building S3 asset keys and setting attributes["name"]; most handlers fall back to "unnamed" when sanitized result is falsy, while one generic asset endpoint uses the sanitized value directly.
Issue attachment endpoints
apps/api/plane/api/views/issue.py, apps/api/plane/app/views/issue/attachment.py
Sanitizes name from request.data before validating/constructing S3 keys and persisting attributes["name"]; V2 endpoint uses "unnamed" fallback when sanitized result is falsy.
Space-scoped asset endpoint
apps/api/plane/space/views/asset.py
Sanitizes incoming name (falls back to "unnamed" when falsy) before using it for S3 asset_key and asset.attributes["name"].

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Endpoint as Upload Endpoint
    participant Sanitizer as sanitize_filename()
    participant Model as get_upload_path / Asset Model
    participant S3 as S3 / Storage

    Client->>Endpoint: POST upload (filename/name)
    Endpoint->>Sanitizer: sanitize_filename(name)
    Sanitizer-->>Endpoint: sanitized_name or None
    alt sanitized_name falsy and endpoint uses fallback
        Endpoint->>Model: use "unnamed" or uuid component to build asset_key
    else sanitized_name truthy
        Endpoint->>Model: use sanitized_name to build asset_key
    end
    Model->>S3: generate/upload path using sanitized component
    S3-->>Endpoint: presigned/post response
    Endpoint-->>Client: response (asset metadata, asset_key)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I tidy names with gentle paws,
I chase the dots and mend the flaws.
A hop, a scrub, a sanitized tune,
Files find their home beneath the moon.
Hooray — no paths that lead to doom.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding filename sanitization to prevent path traversal vulnerabilities in file uploads.
Description check ✅ Passed The description includes a clear summary of changes, context, affected files, and test plan. It addresses the security advisory and explains the rationale for the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sanitize-upload-filenames

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens file upload handling by sanitizing user-supplied filenames before they are incorporated into storage object keys / upload paths, mitigating path traversal–style input and S3 key pollution concerns related to GHSA-v57h-5999-w7xp.

Changes:

  • Added a shared sanitize_filename() utility for upload/path contexts.
  • Applied filename sanitization in get_upload_path() for FileAsset and IssueAttachment.
  • Applied filename sanitization across multiple upload/presign endpoints (app, api, space), including asset duplication key generation.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
apps/api/plane/utils/path_validator.py Introduces sanitize_filename() used across upload flows.
apps/api/plane/db/models/asset.py Sanitizes filename inside FileAsset upload path generation.
apps/api/plane/db/models/issue.py Sanitizes filename inside issue attachment upload path generation.
apps/api/plane/app/views/asset/v2.py Sanitizes request filename across multiple app asset upload endpoints and in duplication key generation.
apps/api/plane/app/views/issue/attachment.py Sanitizes request filename for issue attachment presign flow.
apps/api/plane/api/views/asset.py Sanitizes request filename for user/server/generic asset presign endpoints.
apps/api/plane/api/views/issue.py Sanitizes request filename for issue attachment presign endpoint.
apps/api/plane/space/views/asset.py Sanitizes request filename for space deploy-board asset upload flow.
Comments suppressed due to low confidence (1)

apps/api/plane/api/views/issue.py:1869

  • sanitize_filename(...) returns 'unnamed' for missing/empty input, so name is no longer falsy when the client omits the name field. This makes the later if not name validation ineffective and changes behavior vs the documented request serializer (which requires name). Either validate the raw request.data.get('name') separately or adjust the validation/error messaging and serializer/docs to match the new fallback behavior.
        name = sanitize_filename(request.data.get("name"))
        type = request.data.get("type", False)
        size = request.data.get("size")
        external_id = request.data.get("external_id")
        external_source = request.data.get("external_source")

Comment on lines +28 to +33
# Take only the basename to remove any directory components
filename = os.path.basename(filename)

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

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.path.basename() does not treat backslashes (\) as path separators on POSIX, so inputs like ..\\..\\evil.txt will not have directory components stripped as intended. Consider normalizing separators (e.g., replace \ with /) or explicitly handling Windows paths before taking the basename.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +39
# Remove leading dots (hidden files)
filename = filename.lstrip(".")

# Strip whitespace
filename = filename.strip()

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace is stripped after lstrip('.'), so a filename like ' .env' becomes '.env' after .strip() and the leading dot is no longer removed. Strip whitespace before removing leading dots (or re-run the leading-dot removal after stripping) so hidden-file prefixes can’t bypass sanitization.

Suggested change
# Remove leading dots (hidden files)
filename = filename.lstrip(".")
# Strip whitespace
filename = filename.strip()
# Strip whitespace before removing leading dots so hidden-file
# prefixes cannot be preserved by surrounding spaces.
filename = filename.strip()
# Remove leading dots (hidden files)
filename = filename.lstrip(".")

Copilot uses AI. Check for mistakes.
Comment on lines +502 to 505
name = sanitize_filename(request.data.get("name"))
type = request.data.get("type")
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
project_id = request.data.get("project_id")
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because sanitize_filename(...) falls back to 'unnamed', name will no longer be falsy when the client omits/empties the name field. That makes the later if not name validation/error path ineffective and changes the API behavior vs the documented request serializer (which marks name as required). Consider validating the raw input separately (if name must be provided) or updating the validation/error message and serializer/docs to match the new fallback behavior.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/plane/utils/path_validator.py`:
- Around line 15-33: The sanitize_filename function currently uses
os.path.basename but doesn't normalize Windows-style backslashes, allowing
inputs like "..\\..\\evil.txt" to bypass directory stripping; before calling
os.path.basename in sanitize_filename, replace backslashes with forward slashes
(e.g., filename = filename.replace("\\\\", "/")) so both Windows and POSIX
separators are normalized, then proceed with os.path.basename and the existing
removal of ".." and null bytes to ensure path traversal sequences are reliably
stripped across platforms.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8e1f7f45-8639-4b68-82f2-e06c098e7fcd

📥 Commits

Reviewing files that changed from the base of the PR and between e6b9d4c and dc29407.

📒 Files selected for processing (8)
  • apps/api/plane/api/views/asset.py
  • apps/api/plane/api/views/issue.py
  • apps/api/plane/app/views/asset/v2.py
  • apps/api/plane/app/views/issue/attachment.py
  • apps/api/plane/db/models/asset.py
  • apps/api/plane/db/models/issue.py
  • apps/api/plane/space/views/asset.py
  • apps/api/plane/utils/path_validator.py

- 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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/plane/app/views/asset/v2.py (1)

761-767: ⚠️ Potential issue | 🟠 Major

Duplicate flow still propagates unsanitized filename metadata, and this block fails lint.

Line 761 is currently too long (ruff E501), and Line 764 preserves the original unsanitized name. Reuse a single sanitized value for both destination_key and attributes["name"] to keep behavior consistent and avoid carrying unsafe legacy names forward.

🔧 Proposed fix
-        destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{sanitize_filename(original_asset.attributes.get('name')) or 'unnamed'}"
+        sanitized_name = sanitize_filename(original_asset.attributes.get("name")) or "unnamed"
+        destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{sanitized_name}"
         duplicated_asset = FileAsset.objects.create(
             attributes={
-                "name": original_asset.attributes.get("name"),
+                "name": sanitized_name,
                 "type": original_asset.attributes.get("type"),
                 "size": original_asset.attributes.get("size"),
             },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/app/views/asset/v2.py` around lines 761 - 767, The destination
key and duplicated asset attributes currently use the unsanitized original name
and the long destination_key line triggers ruff E501; compute a sanitized_name
once (e.g., sanitized_name =
sanitize_filename(original_asset.attributes.get("name")) or "unnamed"), use that
sanitized_name when building destination_key and set attributes["name"] to
sanitized_name in the FileAsset.objects.create call (reference symbols:
destination_key, duplicated_asset, FileAsset.objects.create, sanitize_filename,
original_asset.attributes.get("name")) and split/format the destination_key
construction into multiple concatenated parts or use f-string wrapping to
satisfy line-length linting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/api/plane/app/views/asset/v2.py`:
- Around line 761-767: The destination key and duplicated asset attributes
currently use the unsanitized original name and the long destination_key line
triggers ruff E501; compute a sanitized_name once (e.g., sanitized_name =
sanitize_filename(original_asset.attributes.get("name")) or "unnamed"), use that
sanitized_name when building destination_key and set attributes["name"] to
sanitized_name in the FileAsset.objects.create call (reference symbols:
destination_key, duplicated_asset, FileAsset.objects.create, sanitize_filename,
original_asset.attributes.get("name")) and split/format the destination_key
construction into multiple concatenated parts or use f-string wrapping to
satisfy line-length linting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 196afb65-a490-45ad-9714-bcec781e7ac4

📥 Commits

Reviewing files that changed from the base of the PR and between dc29407 and 8f01000.

📒 Files selected for processing (7)
  • apps/api/plane/api/views/asset.py
  • apps/api/plane/app/views/asset/v2.py
  • apps/api/plane/app/views/issue/attachment.py
  • apps/api/plane/db/models/asset.py
  • apps/api/plane/db/models/issue.py
  • apps/api/plane/space/views/asset.py
  • apps/api/plane/utils/path_validator.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/api/plane/db/models/asset.py
  • apps/api/plane/space/views/asset.py
  • apps/api/plane/api/views/asset.py
  • apps/api/plane/utils/path_validator.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants