{"id":"GHSA-6c8g-7p36-r338","summary":"SharpCompress has directory traversal via directory entries in WriteToDirectory (zip slip variant)","details":"### Summary\n\nA path traversal vulnerability in `IArchive.WriteToDirectory()` allows a malicious archive to create directories outside the intended extraction root. For TAR archives, this can be escalated to arbitrary file writes by chaining with a symlink entry, giving a full write primitive on the target filesystem subject to the permissions of the running process.\n\n### Details\n\nThe vulnerable code is in the directory-entry branch of `WriteToDirectoryInternal` (sync, `IArchiveExtensions.cs:48–61`) and `WriteToDirectoryAsyncInternal` (async, `IAsyncArchiveExtensions.cs:70–84`):\n\n```csharp\nvar dirPath = Path.Combine(destinationDirectory, entry.Key);\nDirectory.CreateDirectory(Path.GetDirectoryName(dirPath + \"/\"));\n```\n\nNo `Path.GetFullPath()` normalisation and no bounds check are applied before the `Directory.CreateDirectory` call. Two .NET `Path.Combine` behaviours make this exploitable:\n\n- **Relative traversal**: `Path.Combine(\"/safe/extract\", \"../../evil\")` → the OS resolves `..` segments on the raw path, placing the directory outside the extraction root.\n- **Absolute path override**: `Path.Combine(\"/safe/extract\", \"/tmp/evil\")` → returns `\"/tmp/evil\"` — the base is discarded entirely for rooted paths.\n\nFile entries are **not** directly affected — they route through `ExtractionMethods.WriteEntryToDirectory` which applies the correct guard (`GetFullPath` + `StartsWith`, see `ExtractionMethods.cs:54–65`). The directory-entry branch is a separate fast-path that was added without that guard.\n\nAffected archive formats: ZIP and TAR (non-solid). Solid archives and 7-Zip use the reader path which calls the secure method.\n\n#### Escalation to arbitrary file writes (TAR only)\n\n`Path.GetFullPath` on .NET does not resolve symlinks — it only normalises `.` and `..` segments. This means the file-entry guard in `ExtractionMethods.WriteEntryToDirectory` can be bypassed via symlink chaining in TAR archives when the caller supplies a `SymbolicLinkHandler`:\n\n```csharp\narchive.WriteToDirectory(\"/safe/extract\", new ExtractionOptions\n{\n    ExtractFullPath = true,\n    SymbolicLinkHandler = (linkPath, linkTarget) =\u003e\n        File.CreateSymbolicLink(linkPath, linkTarget)  // naive — no validation of linkTarget\n});\n```\n\nAttack sequence in a single TAR archive:\n\n1. **Symlink entry** — `link` → `../evil_outside/`\n   The `SymbolicLinkHandler` creates `/safe/extract/link` pointing outside the extraction root.\n\n2. **File entry** — `link/secret.txt`\n   `ExtractionMethods.WriteEntryToDirectory` computes:\n   - `destdir = Path.GetFullPath(\"/safe/extract/link\")` → `\"/safe/extract/link\"` — textually inside root, check passes ✓\n   - `File.Open(\"/safe/extract/link/secret.txt\")` — OS follows symlink, file is written to `/evil_outside/secret.txt`\n\nThe library does not validate `linkTarget` before passing it to the caller's handler, and the XML docs do not warn that it may be a traversal path. The idiomatic handler implementation above is therefore silently exploitable.\n\nZIP does not support symlinks in SharpCompress (`ZipEntry.LinkTarget` always returns `null`), so this escalation is TAR-only.\n\n| Attack | ZIP | TAR |\n|--------|-----|-----|\n| Directory traversal (escape extraction root) | Yes | Yes |\n| Escalate to arbitrary file writes via symlink chain | No | Yes (if caller provides `SymbolicLinkHandler`) |\n\n**Recommended fix** — apply the same pattern from `ExtractionMethods.WriteEntryToDirectory` to both affected files:\n\n```csharp\nvar fullDestDir = Path.GetFullPath(destinationDirectory);\nif (!fullDestDir.EndsWith(Path.DirectorySeparatorChar))\n    fullDestDir += Path.DirectorySeparatorChar;\n\nvar dirPath = Path.GetFullPath(Path.Combine(fullDestDir, entry.Key));\nif (!dirPath.StartsWith(fullDestDir, PathComparison))\n    throw new ExtractionException(\n        \"Entry is trying to create a directory outside of the destination directory.\");\n\nDirectory.CreateDirectory(dirPath);\n```\n\nAdditionally, the library should validate `LinkTarget` before invoking the caller's `SymbolicLinkHandler`, or document clearly that callers must validate it themselves.\n\n### PoC\n\nA self-contained .NET console app is available at:\n`https://github.com/svenclaesson/poc-sharpcompress-traversal`\n\n```\ngit clone https://github.com/svenclaesson/poc-sharpcompress-traversal\ncd poc-sharpcompress-traversal\ndotnet run\n```\n\nThe PoC crafts a ZIP with three directory entries (`../../escaped_relative/`, `/tmp/escaped_absolute/`, `safe_subdir/`) using `System.IO.Compression` (stdlib), then extracts with SharpCompress. Output shows `[ESCAPED]` for the two malicious entries and `[ok]` for the legitimate one, on both sync and async APIs.\n\nTested against SharpCompress 0.47.4 (latest NuGet).\n\n### Impact\n\nThis is a path traversal / zip slip vulnerability (CWE-22). Any application that calls `archive.WriteToDirectory()` on an untrusted archive is affected — which covers the primary documented extraction API.\n\nFor ZIP archives the impact is limited to arbitrary directory creation, which can be used to stage privilege escalation (e.g. cron drop-ins, XDG config paths, service spool directories) or shadow expected paths to alter application behaviour.\n\nFor TAR archives, callers that implement a `SymbolicLinkHandler` — which is the only way to faithfully restore a TAR — are exposed to a full arbitrary file write primitive via the symlink chaining described above.","aliases":["CVE-2026-44788"],"modified":"2026-07-21T20:15:26.449621242Z","published":"2026-05-08T23:50:40Z","database_specific":{"nvd_published_at":"2026-05-26T22:16:42Z","cwe_ids":["CWE-22"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-05-08T23:50:40Z"},"references":[{"type":"WEB","url":"https://github.com/adamhathcock/sharpcompress/security/advisories/GHSA-6c8g-7p36-r338"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-44788"},{"type":"WEB","url":"https://github.com/adamhathcock/sharpcompress/commit/2021a06626d0555a4d69471386e763ca5f5d5dfb"},{"type":"PACKAGE","url":"https://github.com/adamhathcock/sharpcompress"}],"affected":[{"package":{"name":"SharpCompress","ecosystem":"NuGet","purl":"pkg:nuget/SharpCompress"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.48.0"}]}],"versions":["0.10.0","0.10.1","0.10.1.1","0.10.1.3","0.10.2","0.10.3","0.11.0","0.11.1","0.11.2","0.11.3","0.11.4","0.11.5","0.11.6","0.12.0","0.12.1","0.12.2","0.12.3","0.12.4","0.13.0","0.13.1","0.14.0","0.14.1","0.15.0","0.15.1","0.15.2","0.16.0","0.16.1","0.16.2","0.17.0","0.17.1","0.18.0","0.18.1","0.18.2","0.19.0","0.19.1","0.19.2","0.2.0","0.20.0","0.21.0","0.21.1","0.22.0","0.23.0","0.24.0","0.25.0","0.25.1","0.26.0","0.27.0","0.27.1","0.28.0","0.28.1","0.28.2","0.28.3","0.29.0","0.3.0","0.30.0","0.30.1","0.31.0","0.32.0","0.32.1","0.32.2","0.33.0","0.34.0","0.34.1","0.34.2","0.35.0","0.36.0","0.37.0","0.37.1","0.37.2","0.38.0","0.39.0","0.4.0","0.40.0","0.41.0","0.42.0","0.42.1","0.43.0","0.44.0","0.44.1","0.44.2","0.44.3","0.44.4","0.44.5","0.45.0","0.45.1","0.46.0","0.46.1","0.46.2","0.46.3","0.46.4","0.47.0","0.47.1","0.47.2","0.47.3","0.47.4","0.5.0","0.6.0","0.7.0","0.8.0","0.8.1","0.8.2","0.9.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-6c8g-7p36-r338/GHSA-6c8g-7p36-r338.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:L"}]}