Skip to content

VPR-138 fix(cms): harden temp file path in DownloadZip#184

Open
rlorenzo wants to merge 1 commit intomainfrom
VPR-138-cms-download-cleanup
Open

VPR-138 fix(cms): harden temp file path in DownloadZip#184
rlorenzo wants to merge 1 commit intomainfrom
VPR-138-cms-download-cleanup

Conversation

@rlorenzo
Copy link
Copy Markdown
Contributor

@rlorenzo rlorenzo commented May 9, 2026

Summary

  • Replace user-influenced temp file path in CMS.DownloadZip with a server-generated GUID under a dedicated temp folder; use FileMode.Create + ZipArchiveMode.Create and delete in finally.
  • Extract sanitization and per-request temp-path generation into CmsFilePathSafety (static + DI-friendly ICmsFilePathSafety for new code); register the new Viper.Areas.CMS.Services namespace with the existing Scrutor scan.
  • Return 400 on empty fileGUIDs and 404 when no files resolve, both before any filesystem I/O.

Test plan

  • test/CMS/DownloadZipSecurityTests.cs — 33 tests covering filename allow-list, reserved Windows device names, extension forcing, traversal regression payloads, and temp-path builder. All pass locally.
  • Full backend suite: 2004/2004 passing.
  • Lint clean on changed files (only pre-existing Program.cs complexity warnings remain).
  • Manual smoke: legitimate ZIP downloads from /CMS/Files still open with sanitized .zip filename in TEST.
  • Manual smoke: traversal payload (fileName=..\..\evil) creates no file outside the temp root.
  • Manual smoke: zero resolvable files returns 404; mixed authorized/unauthorized IDs returns ZIP with only authorized entries.

Summary by CodeRabbit

  • New Features

    • Safer ZIP downloads: filenames and archive entries are sanitized and served with a normalized .zip name; temp archives are created under isolated temp folders with unique paths.
  • Bug Fixes

    • Download endpoint returns proper HTTP errors for empty requests or missing files and ensures temp-file cleanup even on errors.
  • Tests

    • New comprehensive security tests covering filename/entry sanitization, traversal protection, temp-path validation, and reserved-name handling.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • review-ready

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cfc39efe-f5dc-4275-90b1-e324602a66fa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Fixes directory-traversal and temp-file safety in CMS ZIP downloads: add CmsFilePathSafety helpers, refactor DownloadZip to use validated temp paths and sanitized entry/download names, update DI registration, and add comprehensive security tests including a VPR-138 regression.

Changes

CMS DownloadZip Security Hardening

Layer / File(s) Summary
Service Contract & Implementation
web/Areas/CMS/Services/CmsFilePathSafety.cs
Introduces CmsFilePathSafety static utility with SanitizeDownloadName, BuildTempArchivePath, GetZipTempFolder, and SanitizeZipEntryName. Adds ICmsFilePathSafety and sealed CmsFilePathSafetyService adapter delegating to the static methods.
DownloadZip Refactoring
web/Areas/CMS/Data/CMS.cs
Validates inputs (BadRequest for missing GUIDs, NotFound when no files), uses CmsFilePathSafety.SanitizeDownloadName and SanitizeZipEntryName, generates temp archive paths via BuildTempArchivePath, creates zip in isolated temp folder (ZipArchiveMode.Create), serves bytes with MimeTypes["zip"], and performs best-effort cleanup in a finally block.
Dependency Registration
web/Program.cs
Adds Viper.Areas.Curriculum.Services to Scrutor InNamespaces(...) so convention-based *Service/*Validator types are auto-registered.
Security Tests
test/CMS/DownloadZipSecurityTests.cs
Adds comprehensive tests for SanitizeDownloadName (traversal/separators, .zip suffix rules, reserved Windows device names, null/empty fallback, safe characters), SanitizeZipEntryName behavior, BuildTempArchivePath containment/uniqueness/argument validation, and a VPR-138 regression ensuring traversal payloads cannot escape the temp root. Includes helpers for isolated temp directory creation and cleanup.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% 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 describes the main change: hardening temp file path in DownloadZip to address VPR-138, which is the core objective of this PR.
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 VPR-138-cms-download-cleanup

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.

@codecov-commenter
Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 9, 2026

Codecov Report

❌ Patch coverage is 53.19149% with 44 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.99%. Comparing base (4678e73) to head (7ad1880).

Files with missing lines Patch % Lines
web/Areas/CMS/Data/CMS.cs 0.00% 31 Missing ⚠️
web/Areas/CMS/Services/CmsFilePathSafety.cs 81.96% 9 Missing and 2 partials ⚠️
web/Program.cs 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #184      +/-   ##
==========================================
+ Coverage   42.95%   42.99%   +0.03%     
==========================================
  Files         876      877       +1     
  Lines       51454    51525      +71     
  Branches     4802     4811       +9     
==========================================
+ Hits        22101    22151      +50     
- Misses      28829    28848      +19     
- Partials      524      526       +2     
Flag Coverage Δ
backend 43.07% <53.19%> (+0.03%) ⬆️
frontend 41.34% <ø> (ø)

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

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@rlorenzo
Copy link
Copy Markdown
Contributor Author

rlorenzo commented May 9, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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: 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 `@test/CMS/DownloadZipSecurityTests.cs`:
- Around line 5-9: Update the XML doc comment in the CMS.DownloadZip security
tests to use inline code formatting instead of an unresolved <see/> tag: replace
the <see cref="CMS.DownloadZip"/> reference with <c>CMS.DownloadZip</c> in the
comment above the tests (the summary for the tests covering CMS.DownloadZip,
filename sanitizer and temp-archive path builder) to remove the analyzer
warning.

In `@web/Areas/CMS/Data/CMS.cs`:
- Around line 491-505: Sanitize the ZIP entry names before creating entries:
instead of passing file.FriendlyName directly to archive.CreateEntry and
archive.CreateEntryFromFile, strip any path characters (e.g., via
Path.GetFileName or by calling the existing
CmsFilePathSafety.SanitizeDownloadName()) and use that sanitized name; update
the block that handles encrypted files (where DecryptFile is used and writer
writes to fileEntry) and the else branch (where CreateEntryFromFile is called)
to supply the sanitized filename variable.

In `@web/Areas/CMS/Services/CmsFilePathSafety.cs`:
- Around line 48-50: Normalize backslashes before extracting the filename and
ensure any remaining traversal dots are removed: in CmsFilePathSafety.cs, before
calling Path.GetFileName(userInput) replace backslashes with forward slashes
(e.g., userInput.Replace('\\','/')) so Path.GetFileName treats sequences like
"\..\evil.zip" as path segments on Unix, then after applying
SafeFileNameAllowList and Trim remove any remaining ".." sequences (e.g., strip
or collapse runs of "..") so
SanitizeDownloadName_StripsPathSeparatorsAndTraversal and VPR-138 tests no
longer fail.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 84f82321-14d3-46c2-b06b-32c07318742d

📥 Commits

Reviewing files that changed from the base of the PR and between 4678e73 and 0ad937b.

📒 Files selected for processing (4)
  • test/CMS/DownloadZipSecurityTests.cs
  • web/Areas/CMS/Data/CMS.cs
  • web/Areas/CMS/Services/CmsFilePathSafety.cs
  • web/Program.cs

Comment thread test/CMS/DownloadZipSecurityTests.cs
Comment thread web/Areas/CMS/Data/CMS.cs Outdated
Comment thread web/Areas/CMS/Services/CmsFilePathSafety.cs Outdated
Copy link
Copy Markdown

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 the legacy CMS ZIP download flow by separating user-provided download names (response header only) from server-generated on-disk temp archive paths, reducing the risk of path traversal and unsafe filesystem writes.

Changes:

  • Introduces CmsFilePathSafety (static + DI-friendly interface/service) for sanitizing download names, ZIP entry names, and generating per-request temp archive paths.
  • Updates CMS.DownloadZip to validate inputs earlier (400/404), generate temp ZIP paths under a dedicated temp folder, create archives with FileMode.Create/ZipArchiveMode.Create, and always attempt cleanup in finally.
  • Adds a focused security test suite covering sanitization and temp-path generation regressions.

Reviewed changes

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

File Description
web/Program.cs Adds Viper.Areas.CMS.Services to the Scrutor namespace scan for auto DI registration.
web/Areas/CMS/Services/CmsFilePathSafety.cs Adds centralized path/filename sanitization and temp archive path generation helpers (+ DI wrapper).
web/Areas/CMS/Data/CMS.cs Reworks DownloadZip to use the new safety helpers, improve early validation, and ensure temp file cleanup.
test/CMS/DownloadZipSecurityTests.cs Adds security regression tests for download-name sanitization, temp-path containment, and ZIP entry naming.

Comment thread web/Areas/CMS/Services/CmsFilePathSafety.cs
Comment thread web/Areas/CMS/Data/CMS.cs Outdated
@rlorenzo rlorenzo force-pushed the VPR-138-cms-download-cleanup branch from 2531897 to caaaceb Compare May 9, 2026 07:12
@rlorenzo
Copy link
Copy Markdown
Contributor Author

rlorenzo commented May 9, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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: 2

🤖 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 `@web/Areas/CMS/Services/CmsFilePathSafety.cs`:
- Around line 54-58: The allow-list filtering can leave names that consist only
of dots/spaces (e.g., "." or "..") which then become unsafe when suffixed (e.g.,
"..zip"); in the method using SafeFileNameAllowList with variable fileNamePart,
after computing filtered = SafeFileNameAllowList.Replace(fileNamePart,
string.Empty).Trim() treat any filtered value that consists only of dots and/or
whitespace as invalid and return DefaultDownloadName instead; update the
conditional that checks string.IsNullOrEmpty(filtered) (in CmsFilePathSafety.cs
/ the method handling download name normalization) to also detect and reject
dot-only strings (for example by checking filtered.Trim('.') == string.Empty or
equivalent) so such inputs fall back to FileDownload.zip.
- Around line 124-133: The extracted basename in SanitizeZipEntryName (the block
that computes entry = Path.GetFileName(friendlyName.Replace('\\','/')) and
returns it) must reject names that collapse to "." or ".." or consist only of
dots/spaces; update the logic to treat such values as invalid and fall back to
the sanitized fallback basename (i.e., return
Path.GetFileName(fallback.Replace('\\','/')) when entry is null/whitespace or
matches a pattern of only dots/spaces or exactly "." or ".."); also add a
regression unit test next to SanitizeZipEntryName_StripsPathComponents covering
inputs like "..", "dir/..", and @"nested\." to assert the fallback is used.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 31654e00-bc72-46a1-8d47-7691475ace9a

📥 Commits

Reviewing files that changed from the base of the PR and between 0ad937b and caaaceb.

📒 Files selected for processing (4)
  • test/CMS/DownloadZipSecurityTests.cs
  • web/Areas/CMS/Data/CMS.cs
  • web/Areas/CMS/Services/CmsFilePathSafety.cs
  • web/Program.cs

Comment thread web/Areas/CMS/Services/CmsFilePathSafety.cs
Comment thread web/Areas/CMS/Services/CmsFilePathSafety.cs
- Replace user-influenced temp file path with a server-generated GUID
  under a dedicated temp folder; use FileMode.Create +
  ZipArchiveMode.Create and delete in a finally block.
- Add CmsFilePathSafety helper (static + DI variant) for download-name
  sanitization, ZIP-entry sanitization, and per-request temp-archive
  path generation. Register the new Viper.Areas.CMS.Services namespace
  with the existing Scrutor scan.
- SanitizeDownloadName normalizes backslash to forward slash before
  Path.GetFileName so Windows-style traversal payloads ("..\evil") are
  stripped on Linux runners too. Allow-list, reserved Windows device
  names, and forced .zip suffix come along.
- SanitizeZipEntryName strips path components from CMSFile.FriendlyName
  before passing it to archive.CreateEntry/CreateEntryFromFile -
  defense in depth against ZIP-slip when stored names contain
  separators or traversal sequences.
- BuildTempArchivePath trims trailing separators on the resolved root
  so the StartsWith containment check survives a "C:\Temp\\"-style
  input.
- DownloadZip writes encrypted bytes directly to the ZipArchiveEntry
  stream instead of wrapping a StreamWriter (the text encoder was
  unused). Return 400 on empty fileGUIDs and 404 when no files resolve,
  both before any filesystem I/O.
- Add 42 unit tests covering the sanitizer (separators, traversal,
  reserved names, extension forcing), ZIP entry helper, temp-path
  builder (incl. trailing separator), and a parameterized traversal
  regression guard.
@rlorenzo rlorenzo force-pushed the VPR-138-cms-download-cleanup branch from caaaceb to 7ad1880 Compare May 9, 2026 15:57
@rlorenzo rlorenzo requested a review from Copilot May 9, 2026 16:25
Copy link
Copy Markdown

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

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

Comments suppressed due to low confidence (1)

web/Areas/CMS/Data/CMS.cs:459

  • DownloadZip only checks fileGUIDs.Length == 0, but the controller passes ids.Split(','), which can include empty/whitespace entries (e.g. ids=, or trailing commas). That currently falls through and returns 404 after doing work. Consider trimming/filtering out empty GUID strings up front and returning 400 when no non-empty IDs remain to match the intended “empty fileGUIDs => 400” behavior.
            if (fileGUIDs.Length == 0)
            {
                return controller.BadRequest("Missing fileGUIDs parameter.");
            }

            var safeDownloadName = CmsFilePathSafety.SanitizeDownloadName(fileName);

            List<CMSFile> files = new();
            AaudUser? currentUser = UserHelper.GetCurrentUser();

            foreach (var guid in fileGUIDs)

Comment on lines +41 to +56
public static string SanitizeDownloadName(string userInput)
{
if (string.IsNullOrWhiteSpace(userInput))
{
return DefaultDownloadName;
}

// Normalize backslash to forward slash so Path.GetFileName strips
// path components on every OS. Path.GetFileName only honors the
// host OS separator, so a Windows-style "\..\evil" leaks through
// unchanged on Linux runners.
var normalized = userInput.Replace('\\', '/');
var fileNamePart = Path.GetFileName(normalized);
var filtered = SafeFileNameAllowList.Replace(fileNamePart, string.Empty).Trim();

// Dots and spaces survive the allow-list. Names that collapse to
Comment thread web/Areas/CMS/Data/CMS.cs
Comment on lines +387 to +389
#region Download helpers (moved to Viper.Areas.CMS.Services.CmsFilePathSafety)
#endregion

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.

3 participants