From 036e11fe2d4c02c2c1858c0304e4ae4ef856e832 Mon Sep 17 00:00:00 2001 From: Flor Elisa Chacon Ochoa Date: Tue, 7 Jul 2026 16:26:49 -0700 Subject: [PATCH 1/2] Ensure relative paths are rooted --- src/AppInstallerCLITests/Filesystem.cpp | 72 +++++++++++++++++++++--- src/AppInstallerSharedLib/Filesystem.cpp | 22 +++++++- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/src/AppInstallerCLITests/Filesystem.cpp b/src/AppInstallerCLITests/Filesystem.cpp index 12684f19d5..95ea62c493 100644 --- a/src/AppInstallerCLITests/Filesystem.cpp +++ b/src/AppInstallerCLITests/Filesystem.cpp @@ -12,15 +12,69 @@ using namespace TestCommon; TEST_CASE("PathEscapesDirectory", "[filesystem]") { - std::string badRelativePath = "../../target.exe"; - std::string badRelativePath2 = "test/../../target.exe"; - std::string goodRelativePath = "target.exe"; - std::string goodRelativePath2 = "test/../test1/target.exe"; - - REQUIRE(PathEscapesBaseDirectory(badRelativePath)); - REQUIRE(PathEscapesBaseDirectory(badRelativePath2)); - REQUIRE_FALSE(PathEscapesBaseDirectory(goodRelativePath)); - REQUIRE_FALSE(PathEscapesBaseDirectory(goodRelativePath2)); + SECTION("Simple relative paths stay within the base directory") + { + REQUIRE_FALSE(PathEscapesBaseDirectory("target.exe")); + REQUIRE_FALSE(PathEscapesBaseDirectory("test\\target.exe")); + REQUIRE_FALSE(PathEscapesBaseDirectory("test/subdir/target.exe")); + } + + SECTION("Relative paths whose '..' components resolve back inside do not escape") + { + REQUIRE_FALSE(PathEscapesBaseDirectory("test/../test1/target.exe")); + REQUIRE_FALSE(PathEscapesBaseDirectory("./target.exe")); + REQUIRE_FALSE(PathEscapesBaseDirectory("a/b/../../c.exe")); + } + + SECTION("Paths that resolve to the base directory itself do not escape") + { + REQUIRE_FALSE(PathEscapesBaseDirectory(".")); + REQUIRE_FALSE(PathEscapesBaseDirectory("test/..")); + } + + SECTION("An empty path refers to the base directory itself and does not escape") + { + REQUIRE_FALSE(PathEscapesBaseDirectory("")); + } + + SECTION("Relative paths that traverse above the base directory escape") + { + REQUIRE(PathEscapesBaseDirectory("../../target.exe")); + REQUIRE(PathEscapesBaseDirectory("test/../../target.exe")); + REQUIRE(PathEscapesBaseDirectory("../target.exe")); + REQUIRE(PathEscapesBaseDirectory("..")); + REQUIRE(PathEscapesBaseDirectory("a/../../b.exe")); + + // Mixed separators are still normalized correctly. + REQUIRE(PathEscapesBaseDirectory("test\\../..\\target.exe")); + } + + SECTION("Absolute paths escape the base directory") + { + REQUIRE(PathEscapesBaseDirectory("C:\\Windows\\target.exe")); + REQUIRE(PathEscapesBaseDirectory("C:/Windows/target.exe")); + } + + SECTION("UNC paths in their various forms escape the base directory") + { + REQUIRE(PathEscapesBaseDirectory("\\\\server\\share\\target.exe")); + REQUIRE(PathEscapesBaseDirectory("//server/share/target.exe")); + + // Extended-length prefix. + REQUIRE(PathEscapesBaseDirectory("\\\\?\\C:\\target.exe")); + } + + SECTION("Root-relative paths (no drive) resolve to the root of the base directory's drive") + { + REQUIRE(PathEscapesBaseDirectory("\\Windows\\target.exe")); + REQUIRE(PathEscapesBaseDirectory("/Windows/target.exe")); + } + + SECTION("Drive-relative paths resolve against the current directory of the given drive") + { + REQUIRE(PathEscapesBaseDirectory("C:target.exe")); + REQUIRE(PathEscapesBaseDirectory("C:")); + } } TEST_CASE("VerifySymlink", "[filesystem]") diff --git a/src/AppInstallerSharedLib/Filesystem.cpp b/src/AppInstallerSharedLib/Filesystem.cpp index 85acbf09a3..90394c6824 100644 --- a/src/AppInstallerSharedLib/Filesystem.cpp +++ b/src/AppInstallerSharedLib/Filesystem.cpp @@ -372,9 +372,25 @@ namespace AppInstaller::Filesystem bool PathEscapesBaseDirectory(std::string_view relativePath) { - // Normalize the path, then check if the first part is ".." - auto resolvedPath = std::filesystem::path{ relativePath }.lexically_normal(); - return !resolvedPath.empty() && *resolvedPath.begin() == ".."; + std::filesystem::path path{ relativePath }; + + // Reject any path that has a root component. This covers absolute paths (e.g. "C:\foo"), + // drive-relative paths (e.g. "C:foo") and root-relative paths (e.g. "\foo"). + if (path.has_root_path()) + { + return true; + } + + // Resolve any "." and ".." components lexically (i.e. without touching the filesystem). + auto resolvedPath = path.lexically_normal(); + + // If the normalized path still begins with "..", it points to a parent of the base directory. + if (!resolvedPath.empty() && *resolvedPath.begin() == "..") + { + return true; + } + + return false; } // Complicated rename algorithm due to somewhat arbitrary failures. From a8182ec650f836f84e99f101df14290680e9c5cb Mon Sep 17 00:00:00 2001 From: Flor Elisa Chacon Ochoa Date: Wed, 8 Jul 2026 17:29:22 -0700 Subject: [PATCH 2/2] Update release notes --- doc/ReleaseNotes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 28f90d9b5d..7a7640c273 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -1,7 +1,8 @@ -## New in v1.29 +## New in v1.30 Nothing yet. ## Bug Fixes * Fixed a crash (`0x8000ffff`) when using `--disable-interactivity` with the Resume experimental feature enabled during install operations. +* Fixed relative path handling for rooted paths.