microsoft/aspire · error · InvalidDataException

Aspire skills archive entry

Error message

Aspire skills archive entry '{0}' is not safe.

What it means

GetSafeArchiveDestinationPath validates each archive entry name before writing it to disk. Rooted names, empty names, or segments failing the portability check (including '..') are rejected with this InvalidDataException, preventing zip-slip style attacks during extraction.

Solutions

  1. Repackage the archive so every entry is a clean relative path under a single root folder
  2. Strip absolute prefixes and '..' segments from entry names before archiving
  3. Re-download the official archive; do not extract untrusted third-party versions

Example fix

// before (tar entry)
../../../home/user/.bashrc
// after
cskills/my-skill/SKILL.md
Defensive patterns

Strategy: validation

Validate before calling

var name = entry.Name.Replace('\\', '/');
var segs = name.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (Path.IsPathRooted(name) || segs.Length == 0 || segs.Contains(".."))
    throw new Exception($"Unsafe archive entry '{entry.Name}'.");

Try / catch

try { await provider.CreateAsync(...); }
catch (InvalidDataException ex) when (ex.Message.Contains("entry") && ex.Message.Contains("is not safe")) { /* reject/repackage the archive */ }

Prevention

When it happens

Trigger: ExtractArchive extracts an archive entry whose name is absolute ('/x'), empty, or contains non-portable segments ('../secrets', reserved device names), causing GetSafeArchiveDestinationPath to throw.

Common situations: Malicious or corrupted archives with traversal entries; archives built on Windows with backslash-absolute or drive-letter paths; tooling that emits entries like './..' or '//'.

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


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/1a215cb945844b23. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Cli/Agents/AspireSkills/AspireSkillsBundleProvider.cs:562

            var destinationFileDirectory = Path.GetDirectoryName(destinationPath);
            if (!string.IsNullOrEmpty(destinationFileDirectory))
            {
                Directory.CreateDirectory(destinationFileDirectory);
            }

            entry.ExtractToFile(destinationPath, overwrite: false);
        }
    }

    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);

View on GitHub (pinned to 25830f84bd)