lostindark/DriverStoreExplorer · error · InvalidOperationException
Update package contains a file that escapes the application
Error message
Update package contains a file that escapes the application directory: {relativePath} What it means
Zip-slip / path-traversal defense inside ApplyUpdateAsync. After extraction, every file's destination is normalized with Path.GetFullPath and required to remain inside appDir (prefix match on appDir + separator). Any entry whose relative path resolves outside the application folder — via '..' segments or absolute paths — aborts the update before a single file is copied or the running exe is renamed. This is the mitigation for the classic zip-slip class (CVE-2018-1002208-family) and the throw is intentional fail-closed behavior.
Source
Thrown at Rapr/UpdateManager.cs:153
if (subDirs.Length == 1 && Directory.GetFiles(tempExtractPath).Length == 0)
{
sourceDir = subDirs[0];
}
string appDir = Path.GetFullPath(DSEFormHelper.GetApplicationFolder());
string currentExePath = Assembly.GetExecutingAssembly().Location;
// Validate all extracted file paths before making any changes
var filesToCopy = Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories);
foreach (var file in filesToCopy)
{
string relativePath = file.Substring(sourceDir.Length + 1);
string destPath = Path.GetFullPath(Path.Combine(appDir, relativePath));
if (!destPath.StartsWith(appDir + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)
&& !destPath.Equals(appDir, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Update package contains a file that escapes the application directory: {relativePath}");
}
}
// Rename the running exe — Windows allows renaming a running executable
string oldExePath = currentExePath + ".old";
if (File.Exists(oldExePath))
{
File.Delete(oldExePath);
}
File.Move(currentExePath, oldExePath);
try
{
// Copy all files from extracted folder to app directory
foreach (var file in filesToCopy)
{
string relativePath = file.Substring(sourceDir.Length + 1);View on GitHub (pinned to 958fcd481b)
Solutions
- Regenerate the update zip from a clean root so every entry is relative to the app directory (e.g. Compress-Archive -Path appdir\* -DestinationPath release.zip).
- Inspect the offending entry: the message embeds {relativePath} — open the zip and confirm whether '..' or an absolute prefix is present.
- If you control packaging, normalise entry names by stripping leading separators and rejecting '..' before zipping.
- Do NOT weaken the guard to allow traversal — it is your primary defense against malicious update packages.
- If the throw is a false positive caused by sourceDir lacking a trailing separator, ensure sourceDir ends with Path.DirectorySeparatorChar before the Substring at line 147.
Example fix
// before (guards only after extraction; traversal entries already on disk briefly)
var filesToCopy = Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories);
foreach (var file in filesToCopy)
{
string relativePath = file.Substring(sourceDir.Length + 1);
string destPath = Path.GetFullPath(Path.Combine(appDir, relativePath));
if (!destPath.StartsWith(appDir + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)
&& !destPath.Equals(appDir, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Update package contains a file that escapes the application directory: {relativePath}");
}
}
// after (reject traversal entries at the ZipArchive level, before extraction)
using (var archive = ZipFile.OpenRead(tempZipPath))
{
string fullAppDir = Path.GetFullPath(appDir) + Path.DirectorySeparatorChar;
foreach (var entry in archive.Entries)
{
string destPath = Path.GetFullPath(Path.Combine(appDir, entry.FullName));
if (!destPath.StartsWith(fullAppDir, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Update package contains an entry that escapes the application directory: {entry.FullName}");
}
}
}
ZipFile.ExtractToDirectory(tempZipPath, tempExtractPath); Defensive patterns
Strategy: validation
Validate before calling
// Standalone path-containment check usable on any candidate zip before applying.
private static void EnsureZipIsContained(string zipPath, string appDir)
{
string fullAppDir = Path.GetFullPath(appDir).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
using (var archive = ZipFile.OpenRead(zipPath))
{
foreach (var entry in archive.Entries)
{
// Reject traversal or absolute entries before they ever touch disk.
string combined = Path.Combine(fullAppDir, entry.FullName);
string resolved = Path.GetFullPath(combined);
if (!resolved.StartsWith(fullAppDir, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"Update package contains an entry that escapes the application directory: {entry.FullName}");
}
}
}
} Type guard
private static bool IsPathInsideDirectory(string path, string dir)
{
string fullDir = Path.GetFullPath(dir).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
string resolved = Path.GetFullPath(path);
return resolved.StartsWith(fullDir, StringComparison.OrdinalIgnoreCase);
} Try / catch
// The throw is correct fail-closed behavior — do not swallow it. Catch only to
// report and abort cleanly, and never to proceed with the apply.
try
{
EnsureZipIsContained(tempZipPath, appDir);
await this.updateManager.ApplyUpdateAsync(this.latestVersionInfo, progress);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("escapes the application directory"))
{
Logger.Error($"Refusing update: {ex.Message}");
MessageBox.Show("The update package is malformed and was rejected for safety.",
Language.Product_Name, MessageBoxButtons.OK, MessageBoxIcon.Error);
// Do NOT retry — treat as a potential supply-chain indicator.
} Prevention
- Treat this throw as a security signal, not a bug to work around — never relax the containment check.
- Build release zips with relative entry names only (Compress-Archive from inside the app dir, not from above it).
- Add the same path-containment check before extraction, not after, so traversal entries never reach disk (see exampleFix).
- Pin update downloads to a specific release tag so a swapped asset triggers this guard loudly rather than silently.
- When authoring the packager, reject entry names containing '..' or starting with a separator/Drive letter at zip time.
When it happens
Trigger: Line 144 enumerates Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories). For each, destPath = Path.GetFullPath(Path.Combine(appDir, relativePath)). If a zip entry name contains '..' (e.g. '..\\..\\evil.dll') or is absolute (e.g. 'C:\\Windows\\System32\\x'), destPath exits appDir and the StartsWith guard at line 150 throws, embedding the offending relativePath in the message.
Common situations: A hand-built or tampered release zip stored absolute paths; a packaging script zipped from the wrong root and preserved '..' segments; an attacker replaced the asset to drop files outside the app folder; a zip tool that preserves POSIX-style traversal entries on Windows; the sourceDir prefix math went wrong because sourceDir has no trailing separator (relativePath computation at line 147).
Related errors
- Download URL is not from a trusted GitHub domain.
- SHA256 hash of the downloaded file does not match the expect
- Failed to restart the application. Please restart manually.
AI-assisted analysis of lostindark/DriverStoreExplorer@958fcd481b (2026-08-13).
Data as JSON: /api/errors/992309e8b5253822.
Report an issue: GitHub.