GHSA-h39g-6x3c-7fq9 is a low-severity (CVSS 3.8) vulnerability in Zio. A fix is available for Zio — see the affected versions and patch details below.
Zio has SubFileSystem Path Confinement Bypass via Unresolved `..` Segment
Real-World Exposure
ZioReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects NuGet packages — download data is not available via public APIs for these ecosystems.
Description
Summary
SubFileSystem fails to confine operations to its declared sub path when the input path is /../ (or equivalents /../, /..\\). This path passes all validation but resolves to the root of the parent filesystem, allowing directory level operations outside the intended boundary.
Affected Component
Zio.UPath.ValidateAndNormalize
Zio.FileSystems.SubFileSystem
UPath.ValidateAndNormalize has a trailing slash optimisation.
if (!processParts && i + 1 == path.Length)
return path.Substring(0, path.Length - 1);
When the input ends with / or \, and processParts is still false, the function strips the trailing separator and returns immediately before the .. resolution logic runs. The input /../ triggers this path: the trailing / is the last character, processParts has not been set (because .. as the first relative segment after root is specifically exempted), so the function returns /.. with the .. segment unresolved.
The resulting UPath with FullName = "/.." is absolute, contains no control characters, and no colon so it passes FileSystem.ValidatePath without rejection.
When this path reaches SubFileSystem.ConvertPathToDelegate:
protected override UPath ConvertPathToDelegate(UPath path)
{
var safePath = path.ToRelative(); // "/..".ToRelative() = ".."
return SubPath / safePath; // "/jail" / ".." = "/" (resolved by Combine)
}
The delegate filesystem receives / (the root) instead of a path under /jail.
Proof of Concept
using Zio;
using Zio.FileSystems;
var root = new MemoryFileSystem();
root.CreateDirectory("/sandbox");
var sub = new SubFileSystem(root, "/sandbox");
Console.WriteLine(sub.DirectoryExists("/../")); // True (sees parent root)
Console.WriteLine(sub.ConvertPathToInternal("/../")); // "/" (parent root path)
Impact
The escape is limited to directory level operations because appending a filename after .. (e.g., /../file.txt) causes normal .. resolution to trigger, which correctly rejects the path as going above root. Only the bare terminal /../ (which strips to /..) survives. This means that exploitability is limited, and this vulnerability does not escalate to file read/write.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| .NETNuGet | Zio | all versions | 0.22.2dotnet add package Zio --version 0.22.2 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for Zio, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update Zio to 0.22.2 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-h39g-6x3c-7fq9 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-h39g-6x3c-7fq9 can be triaged on real exposure rather than presence alone.
Tailored to GHSA-h39g-6x3c-7fq9. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-h39g-6x3c-7fq9 in your dependencies?
O3 Security finds GHSA-h39g-6x3c-7fq9 across NuGet dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.