fix: bound catalog acquisition before pilot staging

This commit is contained in:
codex
2026-08-05 13:21:51 +03:00
parent b83742b3a5
commit d4a4d9acf8
4 changed files with 40 additions and 1 deletions
@@ -9,11 +9,14 @@ namespace Launcher.Next.Infrastructure;
/// </summary> /// </summary>
public sealed class FileSystemCatalogPackageAcquirer(string catalogRoot) : IPackageCatalogPackageAcquirer public sealed class FileSystemCatalogPackageAcquirer(string catalogRoot) : IPackageCatalogPackageAcquirer
{ {
private const int MaxFiles = 256;
private const long MaxBytes = 512L * 1024 * 1024;
public async Task<CatalogPackage?> AcquireAsync(PackageRequirement requirement, CancellationToken cancellationToken) public async Task<CatalogPackage?> AcquireAsync(PackageRequirement requirement, CancellationToken cancellationToken)
{ {
var root = Path.GetFullPath(catalogRoot); var root = Path.GetFullPath(catalogRoot);
var candidate = Path.GetFullPath(Path.Combine(root, requirement.PackageId, requirement.RequiredVersion)); 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; return null;
var manifestPath = Path.Combine(candidate, "package.manifest.json"); var manifestPath = Path.Combine(candidate, "package.manifest.json");
@@ -21,6 +24,10 @@ public sealed class FileSystemCatalogPackageAcquirer(string catalogRoot) : IPack
return null; return null;
var manifest = PackageManifestJson.Parse(await File.ReadAllTextAsync(manifestPath, cancellationToken)); 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); return new CatalogPackage(manifest, candidate);
} }
+7
View File
@@ -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 immutable staging remains the only active package root. These adapters and the
controlled local catalog scenarios are verified by tests; a corporate DFS pilot controlled local catalog scenarios are verified by tests; a corporate DFS pilot
and Autodesk launch with a real roster are not runtime verified. 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.
@@ -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."
}
@@ -82,5 +82,17 @@ public sealed class RosterAdaptersTests
finally { Directory.Delete(root,true); } 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}]}"""; 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}]}""";
} }