Skip to content

Conversation

@alvabba
Copy link
Collaborator

@alvabba alvabba commented Oct 9, 2025

This change addresses a critical security vulnerability in the delete_upload function within services/file_service.py. Previously, the function accepted absolute paths, allowing for potential path traversal attacks where an attacker could delete arbitrary files outside the intended UPLOAD_DIR.

Changes Made:

  • The delete_upload function now strictly resolves the requested file path against the UPLOAD_DIR.
  • Both UPLOAD_DIR and the target path are resolved using Path.resolve() to normalize paths and handle symlinks.
  • A crucial check ensures that the resolved target path is a direct subpath of the resolved UPLOAD_DIR.
  • The function verifies that the target is a regular file (is_file()) to prevent accidental deletion of directories or following malicious symlinks.
  • If any of these security checks fail, the function now returns False immediately, preventing unauthorized file operations.
  • Existing exception logging for actual file removal errors is preserved.

Motivation:
To enhance the security posture of the application by preventing unauthorized file deletion and ensuring that file operations are strictly confined to the designated upload directory.

Summary by CodeRabbit

  • New Features
    • Safer upload deletion: operations are restricted to the designated uploads folder and only remove regular files.
  • Bug Fixes
    • Attempts to delete paths outside the uploads folder are blocked with warnings.
    • Consistent handling of missing files and other errors with clear outcomes.
  • Chores
    • Expanded logging for deletion actions and edge cases.
  • Documentation
    • Updated wording to describe the refined deletion behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 9, 2025

Walkthrough

Refactors delete_upload in services/file_service.py to perform safe, resolved path handling within UPLOAD_DIR, verify targets are files, and add logging for attempts outside the upload directory, non-file targets, success, not found, and other exceptions. Imports pathlib.Path and updates docstring. Signature remains unchanged.

Changes

Cohort / File(s) Summary
File deletion safety and logging
services/file_service.py
Introduced Path-based resolution for UPLOAD_DIR and target paths; enforced in-directory constraint via is_relative_to; verified is_file before unlink; added detailed logging for out-of-scope paths, non-file targets, success, not found, and other errors; minor docstring update; function signature unchanged.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Caller
  participant FS as FileService.delete_upload()
  participant P as Path/FS
  participant L as Logger

  C->>FS: delete_upload(name_or_path)
  FS->>P: Resolve UPLOAD_DIR
  FS->>P: Build target_path (abs or relative to UPLOAD_DIR)
  FS->>P: Resolve target_path
  FS->>P: Check target_path is within UPLOAD_DIR
  alt Target outside UPLOAD_DIR
    FS->>L: warn("Outside upload dir")
    FS-->>C: False
  else Within UPLOAD_DIR
    FS->>P: is_file?
    alt Not a regular file
      FS->>L: warn("Not a file")
      FS-->>C: False
    else Is a file
      FS->>P: unlink()
      FS->>L: info("Deleted")
      FS-->>C: True
    end
  end
  opt Exceptions
    alt FileNotFoundError
      FS->>L: info("Not found")
      FS-->>C: False
    else Other Exception
      FS->>L: exception("Deletion failed")
      FS-->>C: False
    end
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

I nibble on paths with careful delight,
Guarding the burrow from deletions at night.
If you stray past the hedges, I thump a warn,
Only true files get gently shorn.
Logs like carrot trails mark where I’ve been—
Safe hops, clean crops, and a tidy den. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly summarizes the main change—securing the delete_upload function against path traversal—and uses clear, specific language that matches the PR objectives and implementation details without extraneous information.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/secure-delete-upload

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
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

🧹 Nitpick comments (3)
services/file_service.py (3)

236-247: Use Path API consistently for absolute check

Minor polish: prefer Path(...).is_absolute() over os.path.isabs for consistency with pathlib usage.

Apply:

-        if os.path.isabs(name_or_path):
+        if Path(name_or_path).is_absolute():
             target_path = Path(name_or_path)
         else:
             # Construct path relative to UPLOAD_DIR
             target_path = Path(UPLOAD_DIR) / name_or_path

262-267: Tighten control flow per TRY300

Move the trailing return into the except or drop the final return. This satisfies the linter and clarifies flow.

Apply:

-    except Exception:
-        logger.exception("Failed to delete upload: %s", name_or_path)
-    return False
+    except Exception:
+        logger.exception("Failed to delete upload: %s", name_or_path)
+        return False

248-252: Fallback for Python < 3.9: confirm minimum Python version or add try/except

Use Path.relative_to with ValueError fallback unless you guarantee Python ≥ 3.9:

-        if not target_resolved.is_relative_to(upload_dir_resolved):
+        try:
+            target_resolved.relative_to(upload_dir_resolved)
+        except ValueError:
             logger.warning("Attempt to delete file outside UPLOAD_DIR: %s", name_or_path)
             return False

Ensure your project metadata or CI specifies Python ≥ 3.9.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13271c6 and c9c30ee.

📒 Files selected for processing (1)
  • services/file_service.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.13.3)
services/file_service.py

261-261: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (2)
services/file_service.py (2)

12-12: Import looks good

Using pathlib.Path is appropriate here.


232-234: Docstring and logger init LGTM

Clear intent and consistent logging setup.

Comment on lines +253 to +261
# 2. Verify the target is a regular file and not a directory or symlink pointing outside
if not target_resolved.is_file():
logger.warning("Attempt to delete non-file or invalid file type: %s", name_or_path)
return False

# If all checks pass, remove the file
os.remove(target_resolved)
logger.info("Successfully deleted upload: %s", name_or_path)
return True
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid deleting symlink targets; unlink the symlink itself (safer semantics)

Current logic resolves then deletes the resolved path. If the path provided is a symlink inside UPLOAD_DIR to another file in UPLOAD_DIR, this will delete the target file and leave the symlink, which is surprising and risky. After validating the resolved path is within UPLOAD_DIR, prefer unlinking the symlink itself; otherwise delete the regular file.

Apply:

-        # 2. Verify the target is a regular file and not a directory or symlink pointing outside
-        if not target_resolved.is_file():
-            logger.warning("Attempt to delete non-file or invalid file type: %s", name_or_path)
-            return False
-
-        # If all checks pass, remove the file
-        os.remove(target_resolved)
-        logger.info("Successfully deleted upload: %s", name_or_path)
-        return True
+        # 2. Determine deletion behavior:
+        #    - If the provided path is a symlink, unlink the symlink itself (safe: does not touch target).
+        #    - Otherwise, ensure the resolved target is a regular file and remove it.
+        if target_path.is_symlink():
+            target_path.unlink()
+            logger.info("Successfully deleted upload symlink: %s", name_or_path)
+            return True
+
+        if not target_resolved.is_file():
+            logger.warning("Attempt to delete non-file or invalid file type: %s", name_or_path)
+            return False
+
+        os.remove(target_resolved)
+        logger.info("Successfully deleted upload: %s", name_or_path)
+        return True
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 2. Verify the target is a regular file and not a directory or symlink pointing outside
if not target_resolved.is_file():
logger.warning("Attempt to delete non-file or invalid file type: %s", name_or_path)
return False
# If all checks pass, remove the file
os.remove(target_resolved)
logger.info("Successfully deleted upload: %s", name_or_path)
return True
# 2. Determine deletion behavior:
# - If the provided path is a symlink, unlink the symlink itself (safe: does not touch target).
# - Otherwise, ensure the resolved target is a regular file and remove it.
if target_path.is_symlink():
target_path.unlink()
logger.info("Successfully deleted upload symlink: %s", name_or_path)
return True
if not target_resolved.is_file():
logger.warning("Attempt to delete non-file or invalid file type: %s", name_or_path)
return False
os.remove(target_resolved)
logger.info("Successfully deleted upload: %s", name_or_path)
return True
🧰 Tools
🪛 Ruff (0.13.3)

261-261: Consider moving this statement to an else block

(TRY300)

@fehranbit fehranbit merged commit 9befa49 into main Oct 9, 2025
1 check passed
@fehranbit fehranbit deleted the fix/secure-delete-upload branch October 9, 2025 07:00
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