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

  1. 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).
  2. Inspect the offending entry: the message embeds {relativePath} — open the zip and confirm whether '..' or an absolute prefix is present.
  3. If you control packaging, normalise entry names by stripping leading separators and rejecting '..' before zipping.
  4. Do NOT weaken the guard to allow traversal — it is your primary defense against malicious update packages.
  5. 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

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


AI-assisted analysis of lostindark/DriverStoreExplorer@958fcd481b (2026-08-13). Data as JSON: /api/errors/992309e8b5253822. Report an issue: GitHub.