{"record":{"id":"992309e8b5253822","repo":"lostindark/DriverStoreExplorer","slug":"update-package-contains-a-file-that-escapes-the-ap","errorCode":null,"errorMessage":"Update package contains a file that escapes the application directory: {relativePath}","messagePattern":"Update package contains a file that escapes the application directory: (.+?)","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"Rapr/UpdateManager.cs","lineNumber":153,"sourceCode":"            if (subDirs.Length == 1 && Directory.GetFiles(tempExtractPath).Length == 0)\n            {\n                sourceDir = subDirs[0];\n            }\n\n            string appDir = Path.GetFullPath(DSEFormHelper.GetApplicationFolder());\n            string currentExePath = Assembly.GetExecutingAssembly().Location;\n\n            // Validate all extracted file paths before making any changes\n            var filesToCopy = Directory.GetFiles(sourceDir, \"*\", SearchOption.AllDirectories);\n            foreach (var file in filesToCopy)\n            {\n                string relativePath = file.Substring(sourceDir.Length + 1);\n                string destPath = Path.GetFullPath(Path.Combine(appDir, relativePath));\n\n                if (!destPath.StartsWith(appDir + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)\n                    && !destPath.Equals(appDir, StringComparison.OrdinalIgnoreCase))\n                {\n                    throw new InvalidOperationException($\"Update package contains a file that escapes the application directory: {relativePath}\");\n                }\n            }\n\n            // Rename the running exe — Windows allows renaming a running executable\n            string oldExePath = currentExePath + \".old\";\n            if (File.Exists(oldExePath))\n            {\n                File.Delete(oldExePath);\n            }\n\n            File.Move(currentExePath, oldExePath);\n\n            try\n            {\n                // Copy all files from extracted folder to app directory\n                foreach (var file in filesToCopy)\n                {\n                    string relativePath = file.Substring(sourceDir.Length + 1);","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/lostindark/DriverStoreExplorer/blob/958fcd481bd3e212c3dcf1e531b3e332d5a8e81a/Rapr/UpdateManager.cs#L135-L171","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before (guards only after extraction; traversal entries already on disk briefly)\nvar filesToCopy = Directory.GetFiles(sourceDir, \"*\", SearchOption.AllDirectories);\nforeach (var file in filesToCopy)\n{\n    string relativePath = file.Substring(sourceDir.Length + 1);\n    string destPath = Path.GetFullPath(Path.Combine(appDir, relativePath));\n    if (!destPath.StartsWith(appDir + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)\n        && !destPath.Equals(appDir, StringComparison.OrdinalIgnoreCase))\n    {\n        throw new InvalidOperationException($\"Update package contains a file that escapes the application directory: {relativePath}\");\n    }\n}\n\n// after (reject traversal entries at the ZipArchive level, before extraction)\nusing (var archive = ZipFile.OpenRead(tempZipPath))\n{\n    string fullAppDir = Path.GetFullPath(appDir) + Path.DirectorySeparatorChar;\n    foreach (var entry in archive.Entries)\n    {\n        string destPath = Path.GetFullPath(Path.Combine(appDir, entry.FullName));\n        if (!destPath.StartsWith(fullAppDir, StringComparison.OrdinalIgnoreCase))\n        {\n            throw new InvalidOperationException($\"Update package contains an entry that escapes the application directory: {entry.FullName}\");\n        }\n    }\n}\nZipFile.ExtractToDirectory(tempZipPath, tempExtractPath);","handlingStrategy":"validation","validationCode":"// Standalone path-containment check usable on any candidate zip before applying.\nprivate static void EnsureZipIsContained(string zipPath, string appDir)\n{\n    string fullAppDir = Path.GetFullPath(appDir).TrimEnd(Path.DirectorySeparatorChar)\n                       + Path.DirectorySeparatorChar;\n\n    using (var archive = ZipFile.OpenRead(zipPath))\n    {\n        foreach (var entry in archive.Entries)\n        {\n            // Reject traversal or absolute entries before they ever touch disk.\n            string combined = Path.Combine(fullAppDir, entry.FullName);\n            string resolved = Path.GetFullPath(combined);\n            if (!resolved.StartsWith(fullAppDir, StringComparison.OrdinalIgnoreCase))\n            {\n                throw new InvalidOperationException(\n                    $\"Update package contains an entry that escapes the application directory: {entry.FullName}\");\n            }\n        }\n    }\n}","typeGuard":"private static bool IsPathInsideDirectory(string path, string dir)\n{\n    string fullDir = Path.GetFullPath(dir).TrimEnd(Path.DirectorySeparatorChar)\n                   + Path.DirectorySeparatorChar;\n    string resolved = Path.GetFullPath(path);\n    return resolved.StartsWith(fullDir, StringComparison.OrdinalIgnoreCase);\n}","tryCatchPattern":"// The throw is correct fail-closed behavior — do not swallow it. Catch only to\n// report and abort cleanly, and never to proceed with the apply.\ntry\n{\n    EnsureZipIsContained(tempZipPath, appDir);\n    await this.updateManager.ApplyUpdateAsync(this.latestVersionInfo, progress);\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"escapes the application directory\"))\n{\n    Logger.Error($\"Refusing update: {ex.Message}\");\n    MessageBox.Show(\"The update package is malformed and was rejected for safety.\",\n        Language.Product_Name, MessageBoxButtons.OK, MessageBoxIcon.Error);\n    // Do NOT retry — treat as a potential supply-chain indicator.\n}","preventionTips":["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."],"tags":["security","zip-slip","path-traversal","update","fail-closed","cve-2018-1002208"],"backgroundTag":null,"analyzedSha":"958fcd481bd3e212c3dcf1e531b3e332d5a8e81a","analyzedAt":"2026-08-13T19:08:09.376Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}