{"record":{"id":"1a215cb945844b23","repo":"microsoft/aspire","slug":"aspire-skills-archive-entry-0-is-not-safe","errorCode":null,"errorMessage":"Aspire skills archive entry '{0}' is not safe.","messagePattern":"Aspire skills archive entry '(.+?)' is not safe\\.","errorType":"exception","errorClass":"InvalidDataException","httpStatus":null,"severity":"error","filePath":"src/Aspire.Cli/Agents/AspireSkills/AspireSkillsBundleProvider.cs","lineNumber":562,"sourceCode":"            var destinationFileDirectory = Path.GetDirectoryName(destinationPath);\n            if (!string.IsNullOrEmpty(destinationFileDirectory))\n            {\n                Directory.CreateDirectory(destinationFileDirectory);\n            }\n\n            entry.ExtractToFile(destinationPath, overwrite: false);\n        }\n    }\n\n    private static string GetSafeArchiveDestinationPath(string destinationRoot, string entryName)\n    {\n        var normalizedEntryName = entryName.Replace('\\\\', '/');\n        var segments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);\n        if (Path.IsPathRooted(normalizedEntryName) ||\n            segments.Length == 0 ||\n            segments.Any(static segment => !IsPortablePathSegment(segment)))\n        {\n            throw new InvalidDataException(string.Format(CultureInfo.InvariantCulture, \"Aspire skills archive entry '{0}' is not safe.\", entryName));\n        }\n\n        var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, normalizedEntryName.Replace('/', Path.DirectorySeparatorChar)));\n        if (!destinationPath.StartsWith(destinationRoot + Path.DirectorySeparatorChar, StringComparison.Ordinal) &&\n            !string.Equals(destinationPath, destinationRoot, StringComparison.Ordinal))\n        {\n            throw new InvalidDataException(string.Format(CultureInfo.InvariantCulture, \"Aspire skills archive entry '{0}' escapes the extraction directory.\", entryName));\n        }\n\n        return destinationPath;\n    }\n\n    private static DirectoryInfo FindBundleRoot(string extractionDirectory)\n    {\n        var rootManifestPath = Path.Combine(extractionDirectory, ManifestFileName);\n        if (File.Exists(rootManifestPath))\n        {\n            return new DirectoryInfo(extractionDirectory);","sourceCodeStart":544,"sourceCodeEnd":580,"githubUrl":"https://github.com/microsoft/aspire/blob/25830f84bd145686607ad00c057b3f84e2e51d43/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsBundleProvider.cs#L544-L580","documentation":"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.","triggerScenarios":"ExtractArchive extracts an archive entry whose name is absolute ('/x'), empty, or contains non-portable segments ('../secrets', reserved device names), causing GetSafeArchiveDestinationPath to throw.","commonSituations":"Malicious or corrupted archives with traversal entries; archives built on Windows with backslash-absolute or drive-letter paths; tooling that emits entries like './..' or '//'.","solutions":["Repackage the archive so every entry is a clean relative path under a single root folder","Strip absolute prefixes and '..' segments from entry names before archiving","Re-download the official archive; do not extract untrusted third-party versions"],"exampleFix":"// before (tar entry)\n../../../home/user/.bashrc\n// after\ncskills/my-skill/SKILL.md","handlingStrategy":"validation","validationCode":"var name = entry.Name.Replace('\\\\', '/');\nvar segs = name.Split('/', StringSplitOptions.RemoveEmptyEntries);\nif (Path.IsPathRooted(name) || segs.Length == 0 || segs.Contains(\"..\"))\n    throw new Exception($\"Unsafe archive entry '{entry.Name}'.\");","typeGuard":null,"tryCatchPattern":"try { await provider.CreateAsync(...); }\ncatch (InvalidDataException ex) when (ex.Message.Contains(\"entry\") && ex.Message.Contains(\"is not safe\")) { /* reject/repackage the archive */ }","preventionTips":["Only extract archives from trusted sources","Pre-scan archives with 'tar -tf' or zip listing for '..' or absolute entries","Package entries under a single root folder"],"tags":["security","archive","path-traversal","extraction"],"backgroundTag":"path-traversal-blocked","analyzedSha":"25830f84bd145686607ad00c057b3f84e2e51d43","analyzedAt":"2026-09-16T11:10:06.193Z","contentChangedAt":"2026-09-16T11:10:06.193Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}