From d4a4d9acf81348e3f7b5a89a6604772b3b7fbc13 Mon Sep 17 00:00:00 2001 From: codex Date: Wed, 5 Aug 2026 13:21:51 +0300 Subject: [PATCH] fix: bound catalog acquisition before pilot staging --- .../FileSystemCatalogPackageAcquirer.cs | 9 ++++++++- docs/architecture/package-activation.md | 7 +++++++ docs/evidence/corporate-catalog-pilot-status.json | 13 +++++++++++++ tests/Launcher.Next.Tests/RosterAdaptersTests.cs | 12 ++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 docs/evidence/corporate-catalog-pilot-status.json diff --git a/Launcher.Next.Infrastructure/FileSystemCatalogPackageAcquirer.cs b/Launcher.Next.Infrastructure/FileSystemCatalogPackageAcquirer.cs index f1632da..493b4c1 100644 --- a/Launcher.Next.Infrastructure/FileSystemCatalogPackageAcquirer.cs +++ b/Launcher.Next.Infrastructure/FileSystemCatalogPackageAcquirer.cs @@ -9,11 +9,14 @@ namespace Launcher.Next.Infrastructure; /// public sealed class FileSystemCatalogPackageAcquirer(string catalogRoot) : IPackageCatalogPackageAcquirer { + private const int MaxFiles = 256; + private const long MaxBytes = 512L * 1024 * 1024; + public async Task AcquireAsync(PackageRequirement requirement, CancellationToken cancellationToken) { var root = Path.GetFullPath(catalogRoot); var candidate = Path.GetFullPath(Path.Combine(root, requirement.PackageId, requirement.RequiredVersion)); - if (!IsWithin(root, candidate) || !Directory.Exists(candidate) || IsReparsePoint(candidate)) + if (!Directory.Exists(root) || IsReparsePoint(root) || !IsWithin(root, candidate) || !Directory.Exists(candidate) || IsReparsePoint(candidate)) return null; var manifestPath = Path.Combine(candidate, "package.manifest.json"); @@ -21,6 +24,10 @@ public sealed class FileSystemCatalogPackageAcquirer(string catalogRoot) : IPack return null; var manifest = PackageManifestJson.Parse(await File.ReadAllTextAsync(manifestPath, cancellationToken)); + if (manifest.Files.Count > MaxFiles || manifest.Files.Sum(file => file.Length) > MaxBytes || + manifest.Files.Any(file => string.IsNullOrWhiteSpace(file.Path) || Path.IsPathRooted(file.Path) || + file.Path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Any(segment => segment is ".." or "."))) + return null; return new CatalogPackage(manifest, candidate); } diff --git a/docs/architecture/package-activation.md b/docs/architecture/package-activation.md index 649e7d6..0fc3c79 100644 --- a/docs/architecture/package-activation.md +++ b/docs/architecture/package-activation.md @@ -40,3 +40,10 @@ filesystem catalog adapter returns a descriptor for acquisition only; local immutable staging remains the only active package root. These adapters and the controlled local catalog scenarios are verified by tests; a corporate DFS pilot and Autodesk launch with a real roster are not runtime verified. + +Catalog acquisition is bounded before staging: the configured catalog root and +package root must not be reparse points, the descriptor must remain below the +configured root, manifests may contain at most 256 files and 512 MiB declared +payload, and absolute or traversal payload paths are rejected. A corporate pilot +requires explicit configured roster and catalog roles; absent roles block before +any corporate payload is read. diff --git a/docs/evidence/corporate-catalog-pilot-status.json b/docs/evidence/corporate-catalog-pilot-status.json new file mode 100644 index 0000000..587f2b9 --- /dev/null +++ b/docs/evidence/corporate-catalog-pilot-status.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 1, + "pilotTooling": "VERIFIED_BY_TESTS", + "realCorporateRosterParsing": "BLOCKED", + "realCorporatePackageAcquisition": "BLOCKED", + "blockReason": "CORPORATE_CATALOG_AND_ROSTER_PATHS_NOT_CONFIGURED", + "localImmutableStaging": "VERIFIED_BY_TESTS", + "controlledOutageRecovery": "VERIFIED_BY_TESTS", + "productionActivation": "NOT_RUN", + "autodeskRuntime": "NOT_RUN", + "productionReadiness": "NOT_ACHIEVED", + "redaction": "No corporate paths, roster contents, package payload, session identity, or machine data were recorded." +} diff --git a/tests/Launcher.Next.Tests/RosterAdaptersTests.cs b/tests/Launcher.Next.Tests/RosterAdaptersTests.cs index 119e0ff..d872b64 100644 --- a/tests/Launcher.Next.Tests/RosterAdaptersTests.cs +++ b/tests/Launcher.Next.Tests/RosterAdaptersTests.cs @@ -82,5 +82,17 @@ public sealed class RosterAdaptersTests finally { Directory.Delete(root,true); } } + [Fact] + public async Task FileSystemCatalog_RejectsTraversalInManifest() + { + var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N")); var packageRoot=Path.Combine(root,"P_A","1.0.0"); Directory.CreateDirectory(packageRoot); + try + { + await File.WriteAllTextAsync(Path.Combine(packageRoot,"package.manifest.json"), """{"schemaVersion":1,"packageId":"P_A","version":"1.0.0","product":"Plant3D","productYear":2026,"architecture":"x64","entryAssembly":"entry.dll","files":[{"path":"../outside.dll","length":1,"sha256":"00"}],"startup":{"type":"netload","command":"P_A"},"rolloutChannel":"stable"}"""); + Assert.Null(await new FileSystemCatalogPackageAcquirer(root).AcquireAsync(new("P_A","Plant3D",2026,"x64","stable","1.0.0","project-a",true),CancellationToken.None)); + } + finally { Directory.Delete(root,true); } + } + private static string Json() => """{"schemaVersion":1,"rosterId":"project-a","product":"Plant3D","year":2026,"architecture":"x64","offlinePolicy":"AllowVerifiedLastKnownGood","packages":[{"id":"P_B","version":"2.0.0","channel":"stable","loadOrder":200},{"id":"P_A","version":"1.0.0","channel":"stable","loadOrder":100}]}"""; }