Skip to content

Security: Prevent zip slip during archive extraction#5112

Open
RishabhJain2018 wants to merge 11 commits into
masterfrom
cursor/fix-zip-slip-extraction-9812
Open

Security: Prevent zip slip during archive extraction#5112
RishabhJain2018 wants to merge 11 commits into
masterfrom
cursor/fix-zip-slip-extraction-9812

Conversation

@RishabhJain2018

@RishabhJain2018 RishabhJain2018 commented Jun 3, 2026

Copy link
Copy Markdown
Member

Summary

Mitigates zip slip (path traversal) when extracting challenge and submission archives.

Changes

  • Add safe_extract_zip_file() in apps/base/utils.py to validate member paths before extraction.
  • Use safe extraction in challenge views, config utilities, and submission workers.

Security impact

Prevents malicious zip archives from writing files outside the intended extraction directory.

Slack Thread

Open in Web Open in Cursor 

Summary by CodeRabbit

  • Bug Fixes
    • Hardened ZIP extraction to prevent unsafe archive entries from escaping the intended destination.
    • Improved validation of challenge configuration and all referenced files to detect and reject unsafe path traversal attempts, returning an “unsafe file paths” error when detected.
    • Ensured ZIP handles are reliably closed during challenge creation and submission ZIP processing.
  • Refactor
    • Centralized safe path joining and ZIP extraction logic for consistent enforcement across the app.
  • Tests
    • Added unit coverage for safe path resolution and safe ZIP extraction behavior.

Validate zip member paths before extractall in the API, config utilities,
and submission workers so archives cannot write outside the target directory.

Co-authored-by: Rishabh Jain <rishabhjain2018@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR hardens ZIP extraction and file path handling against traversal attacks by introducing centralized validation utilities and routing all ZIP operations through them. A new PathTraversalError exception and safe_join_under_root() helper validate that resolved file paths remain within allowed root directories. A safe_extract_zip_file() function pre-validates all ZIP member paths before extraction, preventing zip slip attacks. These utilities are then adopted comprehensively across challenge configuration validation (YAML reading, field validation, file loading), the challenge creation API endpoint (multiple file types with HTTP 406 error responses), and submission worker modules.

Changes

ZIP Extraction and Path-Traversal Security

Layer / File(s) Summary
Path-traversal validation foundation
apps/base/zip_utils.py
PathTraversalError exception and safe_join_under_root(root, *relative_paths) helper validate that path joins remain within a normalized root directory and raise an error if escape is detected.
ZIP slip prevention
apps/base/zip_utils.py
safe_extract_zip_file(zip_ref, destination) validates every member in the ZIP file, ensuring each resolved absolute path stays within the destination root; raises BadZipFile on path-traversal detection before extraction proceeds.
Configuration utils extraction hardening
apps/challenges/challenge_config_utils.py
extract_zip_file() imports safe utilities and adopts safe_extract_zip_file() with try/finally wrapping for safe ZipFile cleanup.
Configuration YAML path validation
apps/challenges/challenge_config_utils.py
YAML reading and field validation methods resolve paths via safe_join_under_root(): is_challenge_config_yaml_html_field_valid() rejects unsafe paths with an "Invalid file path" message, get_value_from_field() returns None for traversal errors, and read_and_validate_yaml() fails validation on path-traversal detection.
Configuration file validation hardening
apps/challenges/challenge_config_utils.py
File validation methods (validate_challenge_logo, validate_evaluation_script_file, validate_challenge_phases) resolve paths safely via safe_join_under_root() and catch path-traversal errors by leaving file fields unset and appending error codes.
Challenge creation endpoint hardening
apps/challenges/views.py
The create_challenge_using_zip_file() endpoint extracts ZIPs safely, resolves all referenced file paths (config, evaluation script, test annotations, descriptions, evaluation details, terms, guidelines, and per-phase files) via safe_join_under_root(), and returns HTTP 406 on path-traversal errors or raises RuntimeError for per-phase failures.
Worker module adoption
scripts/workers/remote_submission_worker.py, scripts/workers/submission_worker.py
Worker extract_zip_file() functions adopt safe_extract_zip_file() with explicit try/finally cleanup.
Path safety and ZIP extraction tests
tests/unit/base/test_zip_utils.py
Unit tests cover safe_join_under_root() allowing nested paths and rejecting traversal, and safe_extract_zip_file() extracting valid members and rejecting unsafe paths with BadZipFile.

Sequence Diagram

sequenceDiagram
  participant Client
  participant create_challenge_endpoint
  participant safe_extract_zip_file
  participant safe_join_under_root
  participant ChallengeConfigValidator
  
  Client->>create_challenge_endpoint: POST challenge zip
  create_challenge_endpoint->>safe_extract_zip_file: extract with member validation
  safe_extract_zip_file->>safe_join_under_root: validate each member path
  safe_join_under_root-->>safe_extract_zip_file: path safe or traversal error
  safe_extract_zip_file-->>create_challenge_endpoint: extracted or BadZipFile
  create_challenge_endpoint->>safe_join_under_root: resolve YAML path
  safe_join_under_root-->>create_challenge_endpoint: safe path or PathTraversalError
  create_challenge_endpoint->>ChallengeConfigValidator: load and validate config
  ChallengeConfigValidator->>safe_join_under_root: resolve evaluation_script
  ChallengeConfigValidator->>safe_join_under_root: resolve test annotations
  ChallengeConfigValidator->>safe_join_under_root: resolve descriptions
  ChallengeConfigValidator-->>create_challenge_endpoint: validated or unsafe path
  create_challenge_endpoint-->>Client: HTTP 200 or HTTP 406
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • Potential Security Vulnerability in Challenge ZIP Extraction #4928: This PR directly addresses the zip slip vulnerability identified in the issue by introducing safe_extract_zip_file() with ZIP member path validation and safe_join_under_root() for safe path resolution, replacing vulnerable extractall() and os.path.join() patterns across the challenge creation endpoint and configuration validation layers.

Possibly related PRs

  • Cloud-CV/EvalAI#5110: Both PRs modify the create_challenge_using_zip_file endpoint in apps/challenges/views.py, though this PR hardens path traversal and ZIP slip vulnerabilities while the other addresses host-team authorization.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: preventing zip slip vulnerability during archive extraction, which is the core security focus across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/fix-zip-slip-extraction-9812

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/base/utils.py`:
- Around line 503-517: The ZIP extraction is protected but YAML-sourced paths
can still traverse out of the extracted tree; add a helper (e.g.,
safe_join_under_root(root, *parts)) that resolves and verifies the final path is
inside root (similar check to safe_extract_zip_file), then replace every place
that builds filesystem paths from YAML values (references to description,
evaluation_details, terms_and_conditions, submission_guidelines,
evaluation_script, test_annotation_file) to call
safe_join_under_root(extracted_root, yaml_value) before any
os.path.isfile/open/stat calls; ensure the helper raises on unsafe targets and
is used everywhere YAML-derived values are passed to isfile() or open().

In `@apps/challenges/challenge_config_utils.py`:
- Around line 96-103: The extract_zip_file function currently opens
zipfile.ZipFile(file_path, mode) into zip_ref and calls
safe_extract_zip_file(zip_ref, output_path) but only calls zip_ref.close() after
that, so if safe_extract_zip_file raises the handle remains open; wrap the
extraction call in a try/finally (or use a context manager) so zip_ref.close()
is always executed, i.e., ensure zip_ref is closed in the finally block even on
exceptions and return only after closing; reference symbols: extract_zip_file,
safe_extract_zip_file, zipfile.ZipFile, zip_ref.close().

In `@scripts/workers/remote_submission_worker.py`:
- Around line 137-140: The import of safe_extract_zip_file at module top-level
causes apps/base/utils.py (with Django/DRF top-level imports) to execute and
crash this worker; to fix, remove the top-level line "from base.utils import
safe_extract_zip_file" and instead perform a lazy/local import inside the
function that uses it (the code around zip_ref =
zipfile.ZipFile(download_location, "r") and safe_extract_zip_file(zip_ref,
extract_location)), or refactor safe_extract_zip_file into a small standalone
helper module with no Django dependencies and import that helper here; update
references to use the newly imported/local function name so extraction occurs
without importing Django during module import.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dd256f7a-b836-45ba-9cb2-0e94968634f5

📥 Commits

Reviewing files that changed from the base of the PR and between 4ae73db and 13924d6.

📒 Files selected for processing (5)
  • apps/base/utils.py
  • apps/challenges/challenge_config_utils.py
  • apps/challenges/views.py
  • scripts/workers/remote_submission_worker.py
  • scripts/workers/submission_worker.py

Comment thread apps/base/utils.py Outdated
Comment on lines +503 to +517
def safe_extract_zip_file(zip_ref, destination):
"""
Extract zip archive members while preventing path traversal (zip slip).
"""
destination_path = os.path.abspath(destination)
for member in zip_ref.namelist():
member_path = os.path.abspath(os.path.join(destination_path, member))
if not (
member_path == destination_path
or member_path.startswith(destination_path + os.sep)
):
raise zipfile.BadZipFile(
"Zip archive contains unsafe file paths."
)
zip_ref.extractall(destination_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

This still leaves a traversal path via YAML-referenced files.

safe_extract_zip_file() only validates archive member paths. In the challenge import flows, the extracted YAML is still used to build paths for files like description, evaluation_details, terms_and_conditions, submission_guidelines, evaluation_script, and test_annotation_file with plain join(..., yaml_value) calls. A config value containing ../ can still escape the extracted tree and turn this into an arbitrary file read even though extraction itself is now bounded. Please add a shared safe_join_under_root()-style helper and use it everywhere a YAML value becomes a filesystem path.

🔐 Suggested direction
+def safe_join_under_root(root, relative_path):
+    candidate = os.path.abspath(os.path.join(root, relative_path))
+    if not (
+        candidate == root
+        or candidate.startswith(root + os.sep)
+    ):
+        raise ValueError("Path escapes extraction root.")
+    return candidate

Use that helper before every isfile() / open() on YAML-derived paths.

As per coding guidelines, apps/**: Django backend. Pay attention to: Safe handling of user-submitted files and Docker image references.

📝 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
def safe_extract_zip_file(zip_ref, destination):
"""
Extract zip archive members while preventing path traversal (zip slip).
"""
destination_path = os.path.abspath(destination)
for member in zip_ref.namelist():
member_path = os.path.abspath(os.path.join(destination_path, member))
if not (
member_path == destination_path
or member_path.startswith(destination_path + os.sep)
):
raise zipfile.BadZipFile(
"Zip archive contains unsafe file paths."
)
zip_ref.extractall(destination_path)
def safe_extract_zip_file(zip_ref, destination):
"""
Extract zip archive members while preventing path traversal (zip slip).
"""
destination_path = os.path.abspath(destination)
for member in zip_ref.namelist():
member_path = os.path.abspath(os.path.join(destination_path, member))
if not (
member_path == destination_path
or member_path.startswith(destination_path + os.sep)
):
raise zipfile.BadZipFile(
"Zip archive contains unsafe file paths."
)
zip_ref.extractall(destination_path)
def safe_join_under_root(root, relative_path):
candidate = os.path.abspath(os.path.join(root, relative_path))
if not (
candidate == root
or candidate.startswith(root + os.sep)
):
raise ValueError("Path escapes extraction root.")
return candidate
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/base/utils.py` around lines 503 - 517, The ZIP extraction is protected
but YAML-sourced paths can still traverse out of the extracted tree; add a
helper (e.g., safe_join_under_root(root, *parts)) that resolves and verifies the
final path is inside root (similar check to safe_extract_zip_file), then replace
every place that builds filesystem paths from YAML values (references to
description, evaluation_details, terms_and_conditions, submission_guidelines,
evaluation_script, test_annotation_file) to call
safe_join_under_root(extracted_root, yaml_value) before any
os.path.isfile/open/stat calls; ensure the helper raises on unsafe targets and
is used everywhere YAML-derived values are passed to isfile() or open().

Comment thread apps/challenges/challenge_config_utils.py
Comment thread scripts/workers/remote_submission_worker.py Outdated
Move safe_extract_zip_file into base.zip_utils for workers without Django
setup and close zip handles on extraction failure.
…on root.

Add safe_join_under_root to block path traversal via challenge config values
and ensure zip handles close on extraction failure in views and workers.
Comment thread apps/challenges/challenge_config_utils.py Fixed
RishabhJain2018 and others added 2 commits June 14, 2026 10:37
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
@RishabhJain2018 RishabhJain2018 marked this pull request as ready for review June 14, 2026 17:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/base/zip_utils.py`:
- Around line 13-19: The path validation in safe_join_under_root() function uses
os.path.abspath() with a string prefix check, which does not prevent
symlink-based path traversal attacks. Replace os.path.abspath() with
os.path.realpath() to resolve symlinks before validation, and replace the string
prefix check with os.path.commonpath() to robustly verify that the resolved
candidate path is contained within the root. Additionally, identify the
duplicate path validation logic in safe_extract_zip_file() (which the comment
indicates also applies to lines 26-34) and refactor it to reuse
safe_join_under_root() instead of reimplementing the same check, ensuring
consistent security enforcement across both functions.

In `@apps/challenges/challenge_config_utils.py`:
- Around line 943-959: The challenge_test_annotation_files list is not being
kept in sync with challenge_phases when a test annotation file is not found. In
the else branch after the isfile check for test_annotation_file_path, the code
appends an error message but does not append None to
self.files["challenge_test_annotation_files"], whereas other error branches do
append None. To maintain list alignment with challenge_phases and prevent
annotation files from being shifted onto wrong phases, append None to
self.files["challenge_test_annotation_files"] after appending the error message
in the else block when the annotation file is not found.

In `@apps/challenges/views.py`:
- Around line 1931-1934: The RuntimeError exceptions raised for unsafe file
paths in the PathTraversalError exception handler blocks are losing their error
message context because the outer exception handler at line 2175 does not read
or preserve the RuntimeError message text, causing requests to fall through to
the generic configuration error response instead of returning the specific
unsafe-path failure signal. Create a dedicated exception class (such as
UnsafeFilePathError) that inherits from an appropriate base exception, replace
both RuntimeError raises (in the blocks at lines 1931-1934 and 1947-1950) with
this new exception type, and update the outer handler at line 2175 to
specifically catch and handle this exception class while properly returning the
unsafe-path response and rolling back the transaction.
- Around line 1488-1490: The image lookup still bypasses the path-traversal
guard by using raw join() and isfile() calls without validating against
challenge_config_root. Apply the same root guard protection used for
challenge_config_root to the image file resolution to prevent malicious YAML
values like ../../shared/logo.png from traversing outside the extracted archive.
Ensure the resolved image path is validated to stay within the
challenge_config_root directory boundary before accessing it, using the same
guarding approach established for the configuration file handling.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: be10aef5-930d-40d8-a5c8-c31223a5d89a

📥 Commits

Reviewing files that changed from the base of the PR and between 71416be and fec47a3.

📒 Files selected for processing (5)
  • apps/base/zip_utils.py
  • apps/challenges/challenge_config_utils.py
  • apps/challenges/views.py
  • scripts/workers/remote_submission_worker.py
  • scripts/workers/submission_worker.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/workers/submission_worker.py
  • scripts/workers/remote_submission_worker.py

Comment thread apps/base/zip_utils.py Outdated
Comment thread apps/challenges/challenge_config_utils.py
Comment thread apps/challenges/views.py
Comment thread apps/challenges/views.py Outdated
@RishabhJain2018 RishabhJain2018 force-pushed the cursor/fix-zip-slip-extraction-9812 branch from 4076770 to 05ae194 Compare June 14, 2026 17:46
Use realpath/commonpath for symlink-safe path checks, guard challenge image
paths, keep annotation file lists aligned, return explicit unsafe-path errors,
and add zip_utils unit tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
tests/unit/base/test_zip_utils.py (1)

48-58: ⚡ Quick win

Assert the all-or-nothing extraction contract.

This test only checks that BadZipFile is raised. Because safe_extract_zip_file() is supposed to validate every member before extractall(), the test would still pass if a future change extracted safe members first and only then failed on the unsafe one. Add a mixed archive and assert nothing was written to extract_dir.

Suggested test tightening
     def test_rejects_unsafe_members(self):
         zip_path = os.path.join(self.root, "unsafe.zip")
         extract_dir = os.path.join(self.root, "unsafe-extracted")
         os.makedirs(extract_dir)

         with zipfile.ZipFile(zip_path, "w") as archive:
+            archive.writestr("ok.txt", "ok")
             archive.writestr("../evil.txt", "bad")

         with zipfile.ZipFile(zip_path, "r") as archive:
             with self.assertRaises(zipfile.BadZipFile):
                 safe_extract_zip_file(archive, extract_dir)
+
+        self.assertFalse(os.path.exists(os.path.join(extract_dir, "ok.txt")))

As per coding guidelines, **/test_*.py changes should "Check that tests actually assert behavior, cover edge cases, and are not silently passing."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/base/test_zip_utils.py` around lines 48 - 58, The
test_rejects_unsafe_members method only verifies that BadZipFile is raised but
does not check that the all-or-nothing extraction contract is maintained.
Strengthen the test by modifying the archive to contain both safe and unsafe
members (e.g., add a safe file like "safe.txt" before the unsafe "../evil.txt"
entry), then after the assertRaises block completes, add an assertion to verify
that extract_dir is empty (using os.listdir or similar). This ensures the test
fails if a future change extracts safe members before failing on the unsafe one,
properly enforcing the all-or-nothing extraction guarantee.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/challenges/views.py`:
- Around line 2219-2223: The PathTraversalError exception handler returns a
response before reaching the shutil.rmtree(BASE_LOCATION) cleanup code, causing
the downloaded archive and extracted tree to remain on disk when a traversal
attempt is rejected, which creates a disk-space leak if the request is repeated.
Move the shutil.rmtree(BASE_LOCATION) cleanup out of the generic exception
branch by wrapping it in a finally block at the appropriate scope level, or use
a TemporaryDirectory wrapper, so the cleanup executes for all code paths
including the early return from the PathTraversalError handler, successful
executions, and other exception scenarios.

---

Nitpick comments:
In `@tests/unit/base/test_zip_utils.py`:
- Around line 48-58: The test_rejects_unsafe_members method only verifies that
BadZipFile is raised but does not check that the all-or-nothing extraction
contract is maintained. Strengthen the test by modifying the archive to contain
both safe and unsafe members (e.g., add a safe file like "safe.txt" before the
unsafe "../evil.txt" entry), then after the assertRaises block completes, add an
assertion to verify that extract_dir is empty (using os.listdir or similar).
This ensures the test fails if a future change extracts safe members before
failing on the unsafe one, properly enforcing the all-or-nothing extraction
guarantee.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: edbb1763-4266-4638-a3d5-673855f566ee

📥 Commits

Reviewing files that changed from the base of the PR and between 05ae194 and 99b0117.

📒 Files selected for processing (4)
  • apps/base/zip_utils.py
  • apps/challenges/challenge_config_utils.py
  • apps/challenges/views.py
  • tests/unit/base/test_zip_utils.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/base/zip_utils.py
  • apps/challenges/challenge_config_utils.py

Comment thread apps/challenges/views.py
Comment on lines +2219 to +2223
except PathTraversalError:
response_data = {
"error": "Challenge configuration contains unsafe file paths."
}
return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move temp-directory cleanup out of the generic-exception branch.

Line 2219 now returns before the only shutil.rmtree(BASE_LOCATION) block, so rejected traversal attempts leave the downloaded archive and extracted tree behind. Repeating that request is enough to turn the hardening path into a disk-space leak; cleanup needs to run from an outer finally or a TemporaryDirectory wrapper so it executes on success and every early return, not just the fallback except.

Suggested fix shape
-    try:
+    try:
         ...
     except PathTraversalError:
         response_data = {
             "error": "Challenge configuration contains unsafe file paths."
         }
         return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE)
     except:  # noqa: E722
         ...
-        finally:
-            try:
-                shutil.rmtree(BASE_LOCATION)
-                logger.info("Zip folder is removed")
-            except:  # noqa: E722
-                logger.exception(
-                    "Zip folder for challenge {} is not removed from {} "
-                    "location".format(challenge.pk, BASE_LOCATION)
-                )
+    finally:
+        try:
+            shutil.rmtree(BASE_LOCATION)
+            logger.info("Zip folder is removed")
+        except Exception:
+            logger.exception(
+                "Zip folder is not removed from %s", BASE_LOCATION
+            )

As per coding guidelines, apps/** changes should pay attention to "Safe handling of user-submitted files and Docker image references."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/challenges/views.py` around lines 2219 - 2223, The PathTraversalError
exception handler returns a response before reaching the
shutil.rmtree(BASE_LOCATION) cleanup code, causing the downloaded archive and
extracted tree to remain on disk when a traversal attempt is rejected, which
creates a disk-space leak if the request is repeated. Move the
shutil.rmtree(BASE_LOCATION) cleanup out of the generic exception branch by
wrapping it in a finally block at the appropriate scope level, or use a
TemporaryDirectory wrapper, so the cleanup executes for all code paths including
the early return from the PathTraversalError handler, successful executions, and
other exception scenarios.

Source: Coding guidelines

Set challenge_config_location in config util tests and expect realpath-
normalized extraction paths in remote submission worker tests.
@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.87755% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.96%. Comparing base (bf07b3e) to head (2966897).

Files with missing lines Patch % Lines
apps/challenges/challenge_config_utils.py 93.87% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5112      +/-   ##
==========================================
+ Coverage   90.92%   90.96%   +0.04%     
==========================================
  Files         110      110              
  Lines        8614     8644      +30     
==========================================
+ Hits         7832     7863      +31     
+ Misses        782      781       -1     
Flag Coverage Δ
backend 93.43% <93.87%> (+0.05%) ⬆️
frontend 87.54% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
Accounts & Authentication 97.46% <ø> (ø)
Challenges Management 95.79% <93.87%> (+0.08%) ⬆️
Job Processing 89.82% <ø> (ø)
Participants & Teams 99.54% <ø> (ø)
Challenge Hosts 100.00% <ø> (ø)
Analytics 100.00% <ø> (ø)
Web Interface 100.00% <ø> (ø)
Frontend (Gulp) 87.54% <ø> (ø)
All Models 97.36% <ø> (ø)
All Views 100.00% <ø> (ø)
All Serializers 98.67% <ø> (ø)
Utility Functions 96.87% <ø> (ø)
Core Configuration 82.35% <ø> (ø)
Files with missing lines Coverage Δ
apps/challenges/views.py 100.00% <ø> (ø)
apps/challenges/challenge_config_utils.py 92.24% <93.87%> (+0.56%) ⬆️
Files with missing lines Coverage Δ
apps/challenges/views.py 100.00% <ø> (ø)
apps/challenges/challenge_config_utils.py 92.24% <93.87%> (+0.56%) ⬆️

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update bf07b3e...2966897. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Cover PathTraversalError branches in zip_utils, challenge config validation, and create_challenge_using_zip_file so patch coverage meets Codecov thresholds.
Initialize test_annotation_file_path before phase validation so PathTraversalError handling does not leave the variable unset.
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