{"record":{"id":"2aefac1528ce5e9f","repo":"microsoft/aspire","slug":"symlink-entry-name-targets-entry-linkname-which-resolves-2aefac","errorCode":null,"errorMessage":"Symlink '{entry.Name}' targets '{entry.LinkName}' which resolves outside the destination directory.","messagePattern":"Symlink '(.+?)' targets '(.+?)' which resolves outside the destination directory\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Aspire.Cli/Utils/ArchiveHelper.cs","lineNumber":126,"sourceCode":"\n                    // Preserve Unix file permissions from tar entry\n                    if (!environment.IsWindows() && entry.Mode != default)\n                    {\n                        File.SetUnixFileMode(fullPath, (UnixFileMode)entry.Mode);\n                    }\n                    break;\n\n                case TarEntryType.SymbolicLink:\n                    if (string.IsNullOrEmpty(entry.LinkName))\n                    {\n                        continue;\n                    }\n                    // Validate symlink target stays within the extraction directory\n                    var linkTarget = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(fullPath)!, entry.LinkName));\n                    if (!linkTarget.StartsWith(normalizedDestination + Path.DirectorySeparatorChar, StringComparison.Ordinal) &&\n                        !linkTarget.Equals(normalizedDestination, StringComparison.Ordinal))\n                    {\n                        throw new InvalidOperationException($\"Symlink '{entry.Name}' targets '{entry.LinkName}' which resolves outside the destination directory.\");\n                    }\n                    var linkDir = Path.GetDirectoryName(fullPath);\n                    if (linkDir is not null)\n                    {\n                        Directory.CreateDirectory(linkDir);\n                    }\n                    if (File.Exists(fullPath))\n                    {\n                        File.Delete(fullPath);\n                    }\n                    File.CreateSymbolicLink(fullPath, entry.LinkName);\n                    break;\n            }\n        }\n    }\n}\n","sourceCodeStart":108,"sourceCodeEnd":143,"githubUrl":"https://github.com/microsoft/aspire/blob/25830f84bd145686607ad00c057b3f84e2e51d43/src/Aspire.Cli/Utils/ArchiveHelper.cs#L108-L143","documentation":"During tar.gz extraction, symlinks are validated by resolving their LinkName relative to the entry's directory; if the resolved target lies outside the destination directory the extraction is aborted with InvalidOperationException. This prevents symlink-based escapes from the extraction root.","triggerScenarios":"Extracting a tar.gz containing a symlink whose LinkName (e.g. '../../../outside' or an absolute path) resolves outside the destination directory.","commonSituations":"Malicious archives that pair in-bounds entries with out-of-bounds symlinks; archives copied from filesystem layouts where relative link targets pointed elsewhere; corrupted archives.","solutions":["Re-download the archive from a trusted source.","Inspect symlinks (tar -tvzf) and repack so link targets resolve inside the archive root.","If you control packaging, avoid emitting symlinks with targets outside the staged directory."],"exampleFix":"// before\n// entry \"link\" -> \"../../../etc/passwd\"\n// after\n// repack: entry \"link\" -> \"./data/file\" (resolves inside destination)","handlingStrategy":"try-catch","validationCode":"await using var stream = File.OpenRead(tarGzPath);\nawait using var gz = new GZipStream(stream, CompressionMode.Decompress);\nusing var tar = new TarReader(gz);\nwhile (await tar.GetNextEntryAsync() is { } e)\n{\n    if (e.EntryType == TarEntryType.SymbolicLink)\n    {\n        var resolved = Path.GetFullPath(Path.Combine(destinationPath, e.LinkName));\n        if (!resolved.StartsWith(Path.GetFullPath(destinationPath) + Path.DirectorySeparatorChar, StringComparison.Ordinal)) throw new InvalidOperationException($\"Unsafe symlink: {e.Name} -> {e.LinkName}\");\n    }\n}","typeGuard":null,"tryCatchPattern":"try { await ArchiveHelper.ExtractAsync(tgzPath, dest, env, ct); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"Symlink\")) { // reject archive with escaping symlink\n}","preventionTips":["Prefer archives without symlinks when packaging.","Verify archive provenance and checksums.","Pre-scan symlink entries and resolve their targets before extraction."],"tags":["cli","tar","symlink","security"],"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-21T09:17:21.228Z"}