GHSA-h39g-6x3c-7fq9
Zio has SubFileSystem Path Confinement Bypass via Unresolved `..` Segment
GHSA-h39g-6x3c-7fq9 is a low vulnerability (CVSS 3.8) in Zio, published April 18, 2026. No source lists public exploit code. Fixed in Zio 0.22.2.
- Severity
- 3.8 LowCVSS 3.1, GitHub Advisories
- Exploit chance
- UnscoredNo EPSS score yet
- Exploitation
- None knownNot on CISA’s or ENISA’s lists
- Fixed in
0.22.2Zio +1 more
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.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- Low
- Integrity
- None
- Availability
- None
Affected packages
| Package | Affected | Fixed |
|---|---|---|
ZioNuGet | through 0.22.1 | 0.22.2 |
ZioNuGet | before 0.22.2, plus listed versions 0.1.00.10.00.11.00.12.00.13.0 Show 40 moreShow less0.14.00.15.00.16.00.16.10.16.20.17.00.17.10.18.00.18.10.19.00.19.10.19.20.2.00.20.00.21.00.21.10.21.20.21.30.22.00.22.10.3.00.3.10.3.20.3.30.3.40.3.50.3.60.4.00.5.00.6.00.7.00.7.10.7.20.7.30.7.40.7.50.7.60.8.00.9.00.9.1 | 0.22.2 |
What each source reports
GitHub Advisories Score used 3.8
Zio has SubFileSystem Path Confinement Bypass via Unresolved `..` Segment
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.
- Record
GHSA-h39g-6x3c-7fq9- CVSS 3.1
- 3.8 Low
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-22, CWE-179
- Updated
- 2026-11-02
OSV 3.8
Zio has SubFileSystem Path Confinement Bypass via Unresolved `..` Segment
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.
- Record
GHSA-h39g-6x3c-7fq9- CVSS 3.1
- 3.8 Low
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-179, CWE-22
- Updated
- 2026-11-02
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | GitHub Advisories, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.