-
Notifications
You must be signed in to change notification settings - Fork 22
Sanitize download filenames and prevent traversal #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -109,6 +109,7 @@ public static bool ModExists(ObservableCollection<Mod> mods, string modName) => | |||||||||||||
| public static void ProcessFile(string file, string destinationDirectory, ProgressWindow progressWindow) | ||||||||||||||
| { | ||||||||||||||
| var extension = Path.GetExtension(file).ToLowerInvariant(); | ||||||||||||||
| var normalizedDestinationDirectory = Path.GetFullPath(destinationDirectory + Path.DirectorySeparatorChar); | ||||||||||||||
|
|
||||||||||||||
| if (!Directory.Exists(destinationDirectory)) | ||||||||||||||
| Directory.CreateDirectory(destinationDirectory); | ||||||||||||||
|
|
@@ -144,12 +145,10 @@ public static void ProcessFile(string file, string destinationDirectory, Progres | |||||||||||||
| ); | ||||||||||||||
|
|
||||||||||||||
| var entryDestinationPath = Path.Combine(destinationDirectory, sanitizedKey); | ||||||||||||||
| var normalizedEntryDestinationPath = Path.GetFullPath(entryDestinationPath); | ||||||||||||||
|
|
||||||||||||||
| // Ensure the entry destination path is within the destination directory | ||||||||||||||
| if ( | ||||||||||||||
| !Path.GetFullPath(entryDestinationPath) | ||||||||||||||
| .StartsWith(Path.GetFullPath(destinationDirectory), StringComparison.OrdinalIgnoreCase) | ||||||||||||||
| ) | ||||||||||||||
| if (!normalizedEntryDestinationPath.StartsWith(normalizedDestinationDirectory, StringComparison.OrdinalIgnoreCase)) | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Find and examine ModInstallation.cs around line 151
find . -name "ModInstallation.cs" -type f | head -5Repository: TeamWheelWizard/WheelWizard Length of output: 125 🏁 Script executed: # Find DownloadHelper to compare the containment check logic
find . -name "DownloadHelper.cs" -type f | head -5Repository: TeamWheelWizard/WheelWizard Length of output: 110 🏁 Script executed: # Examine ModInstallation.cs around line 151
cat -n ./WheelWizard/Services/Installation/ModInstallation.cs | sed -n '140,160p'Repository: TeamWheelWizard/WheelWizard Length of output: 1435 🏁 Script executed: # Examine DownloadHelper.cs to find the containment check logic
cat -n ./WheelWizard/Helpers/DownloadHelper.cs | head -100Repository: TeamWheelWizard/WheelWizard Length of output: 4874 🏁 Script executed: # Continue reading DownloadHelper.cs to find the EnsurePathStaysWithinDirectory method
cat -n ./WheelWizard/Helpers/DownloadHelper.cs | tail -100Repository: TeamWheelWizard/WheelWizard Length of output: 5133 🏁 Script executed: # Find where normalizedDestinationDirectory is defined in ModInstallation.cs
cat -n ./WheelWizard/Services/Installation/ModInstallation.cs | sed -n '100,150p'Repository: TeamWheelWizard/WheelWizard Length of output: 2658 Use OS-aware comparison in Line 151 hardcodes Suggested fix- if (!normalizedEntryDestinationPath.StartsWith(normalizedDestinationDirectory, StringComparison.OrdinalIgnoreCase))
+ var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
+ if (!normalizedEntryDestinationPath.StartsWith(normalizedDestinationDirectory, comparison))
{
throw new UnauthorizedAccessException("Entry is attempting to extract outside of the destination directory.");
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| { | ||||||||||||||
| throw new UnauthorizedAccessException("Entry is attempting to extract outside of the destination directory."); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: TeamWheelWizard/WheelWizard
Length of output: 110
🏁 Script executed:
Repository: TeamWheelWizard/WheelWizard
Length of output: 1674
🏁 Script executed:
Repository: TeamWheelWizard/WheelWizard
Length of output: 118
🏁 Script executed:
Repository: TeamWheelWizard/WheelWizard
Length of output: 1301
Only use the URL extension as a fallback.
Lines 218-220 unconditionally replace the filename's extension using
Path.GetExtension(finalUrl)on a full URL string. This captures query parameters as part of the extension—e.g., a response withfilename="mod.zip"from/download.php?id=1becomesmod.php?id=1, which is invalid. Usenew Uri(finalUrl).AbsolutePathto extract only the path portion, and only apply it whensafeFileNamedoesn't already have a valid extension.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents