// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include #include #include #include #include namespace zen::compute { // Validate that a single path component contains only characters that are valid // file/directory names on all supported platforms. Uses Windows rules as the most // restrictive superset, since packages may be built on one platform and consumed // on another. inline void ValidatePathComponent(std::string_view Component, std::string_view FullPath) { // Reject control characters (0x00-0x1F) and characters forbidden on Windows for (char Ch : Component) { if (static_cast(Ch) < 0x20 || Ch == '<' || Ch == '>' || Ch == ':' || Ch == '"' || Ch == '|' || Ch == '?' || Ch == '*') { throw zen::invalid_argument("invalid character in path component '{}' of '{}'", Component, FullPath); } } // Reject empty components and trailing dots or spaces (silently stripped on Windows, leading to confusion) if (Component.empty() || Component.back() == '.' || Component.back() == ' ') { throw zen::invalid_argument("path component '{}' of '{}' has trailing dot or space", Component, FullPath); } // Reject Windows reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) // These are reserved with or without an extension (e.g. "CON.txt" is still reserved). std::string_view Stem = Component.substr(0, Component.find('.')); static constexpr std::string_view ReservedNames[] = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", }; for (std::string_view Reserved : ReservedNames) { if (zen::StrCaseCompare(Stem, Reserved) == 0) { throw zen::invalid_argument("path component '{}' of '{}' uses reserved device name '{}'", Component, FullPath, Reserved); } } } // Validate that a path extracted from a package is a safe relative path. // Rejects absolute paths, ".." components, and invalid platform filenames. inline void ValidateSandboxRelativePath(std::string_view Name) { if (Name.empty()) { throw zen::invalid_argument("path traversal detected: empty path name"); } std::filesystem::path Parsed(Name); if (Parsed.is_absolute()) { throw zen::invalid_argument("path traversal detected: '{}' is an absolute path", Name); } for (const auto& Component : Parsed) { std::string ComponentStr = Component.string(); if (ComponentStr == "..") { throw zen::invalid_argument("path traversal detected: '{}' contains '..' component", Name); } // Skip "." (current directory) — harmless in relative paths if (ComponentStr != ".") { ValidatePathComponent(ComponentStr, Name); } } } // Validate all path entries in a worker description CbObject. // Checks path, executables[].name, dirs[], and files[].name fields. // Throws an exception if any invalid paths are found. inline void ValidateWorkerDescriptionPaths(const CbObject& WorkerDescription) { using namespace std::literals; if (auto PathField = WorkerDescription["path"sv]; PathField.HasValue()) { ValidateSandboxRelativePath(PathField.AsString()); } for (auto& It : WorkerDescription["executables"sv]) { ValidateSandboxRelativePath(It.AsObjectView()["name"sv].AsString()); } for (auto& It : WorkerDescription["dirs"sv]) { ValidateSandboxRelativePath(It.AsString()); } for (auto& It : WorkerDescription["files"sv]) { ValidateSandboxRelativePath(It.AsObjectView()["name"sv].AsString()); } } } // namespace zen::compute