Monitor vulnerabilities like this one.
Sign up free to get alerted when software you use is affected.
3.8
Zio has SubFileSystem Path Confinement Bypass via Unresolved `..` Segment
GHSA-h39g-6x3c-7fq9
Summary
# 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.
# Aff...
What to do
- Update zio to version 0.22.2.
Affected software
| Ecosystem | Vendor | Product | Affected versions |
|---|---|---|---|
| nuget | – | zio |
<= 0.22.1 Fix: upgrade to 0.22.2
|
Original title
Zio has SubFileSystem Path Confinement Bypass via Unresolved `..` Segment
Original 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.
```csharp
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`:
```csharp
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
```csharp
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.
`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.
```csharp
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`:
```csharp
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
```csharp
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.
ghsa CVSS3.1
3.8
Vulnerability type
CWE-22
Path Traversal
CWE-179
Published: 18 Apr 2026 · Updated: 18 Apr 2026 · First seen: 18 Apr 2026