microsoft/aspire · error · InvalidDataException
Aspire skills archive entry
Error message
Aspire skills archive entry '{0}' escapes the extraction directory. What it means
Even after segment validation, GetSafeArchiveDestinationPath performs a final containment check: the fully-resolved destination must remain under the extraction root. If Path.GetFullPath resolves outside destinationRoot (e.g. via a link or unexpected normalization), this InvalidDataException is thrown as the last line of defense against archive escape.
Solutions
- Repackage or re-download the archive so entries resolve inside the extraction directory
- Ensure the extraction destination directory itself is a real, non-symlinked path
- Inspect entry names with 'tar -tf' and remove entries that resolve outside the root
Example fix
// before (tar entry via symlink) link -> /etc, link/passwd // after skills/my-skill/SKILL.md
Defensive patterns
Strategy: try-catch
Validate before calling
var dest = Path.GetFullPath(Path.Combine(root, entry.Name.Replace('/', Path.DirectorySeparatorChar)));
if (!dest.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.Ordinal))
throw new Exception($"Entry '{entry.Name}' escapes the extraction directory."); Try / catch
try { await provider.CreateAsync(...); }
catch (InvalidDataException ex) when (ex.Message.Contains("escapes the extraction directory")) { /* treat archive as malicious; do not extract */ } Prevention
- Extract into a freshly created, non-symlinked temp directory
- Never extract untrusted archives without a containment check
- Re-verify archives against their published SHA-512 before extraction
When it happens
Trigger: An archive entry whose combined path escapes destinationRoot after full resolution, encountered during ExtractArchive of a skills package.
Common situations: Archives abusing symlinks or crafted names to escape the extraction dir; extracting into an unexpected working directory where the root check fails; corrupted archive metadata.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Aspire skills archive entry
- Symlink ' ' targets ' ' which resolves outside the…
- Tar entry ' ' would extract outside the destination…
- Aspire skills archive entry
- Aspire skills bundle path
AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16).
Data as JSON: /api/errors/043f62102975999d.
Report an issue: GitHub.
Appendix: source
Thrown at src/Aspire.Cli/Agents/AspireSkills/AspireSkillsBundleProvider.cs:569
}
}
private static string GetSafeArchiveDestinationPath(string destinationRoot, string entryName)
{
var normalizedEntryName = entryName.Replace('\\', '/');
var segments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (Path.IsPathRooted(normalizedEntryName) ||
segments.Length == 0 ||
segments.Any(static segment => !IsPortablePathSegment(segment)))
{
throw new InvalidDataException(string.Format(CultureInfo.InvariantCulture, "Aspire skills archive entry '{0}' is not safe.", entryName));
}
var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, normalizedEntryName.Replace('/', Path.DirectorySeparatorChar)));
if (!destinationPath.StartsWith(destinationRoot + Path.DirectorySeparatorChar, StringComparison.Ordinal) &&
!string.Equals(destinationPath, destinationRoot, StringComparison.Ordinal))
{
throw new InvalidDataException(string.Format(CultureInfo.InvariantCulture, "Aspire skills archive entry '{0}' escapes the extraction directory.", entryName));
}
return destinationPath;
}
private static DirectoryInfo FindBundleRoot(string extractionDirectory)
{
var rootManifestPath = Path.Combine(extractionDirectory, ManifestFileName);
if (File.Exists(rootManifestPath))
{
return new DirectoryInfo(extractionDirectory);
}
var packageDirectory = Path.Combine(extractionDirectory, "package");
var packageManifestPath = Path.Combine(packageDirectory, ManifestFileName);
if (File.Exists(packageManifestPath))
{
return new DirectoryInfo(packageDirectory);View on GitHub (pinned to 25830f84bd)