-
Notifications
You must be signed in to change notification settings - Fork 0
Security
Two guarantees ship with every hub: path sandboxing and a runtime read-only wrapper.
Every DirectoryEntry carries a RootPath set by the hub. The driver rejects any operation whose resolved path falls outside that root.
var hub = new LocalFileHub(@"C:\data");
hub.Root.CreateFile("inside.txt"); // OK
hub.Root.CreateDirectory("2026/01/invoices"); // OK — nested paths create intermediates
hub.Root.OpenDirectory("../../Windows"); // FileHubException (".." blocked)
hub.Root.CreateFile("../escape.txt"); // ArgumentException ("/" in a leaf name)
hub.Root.OpenFile("..\\..\\etc\\passwd"); // FileHubException-
ValidateName(name)applies a portable rule set (PathUtil.ValidateName, same on every OS): rejectsnull, empty,.,.., path separators (/,\) and control characters. The Local driver additionally rejects the characters the host OS forbids (PathUtil.ValidateLocalName, viaPath.GetInvalidFileNameChars()). -
ResolveSafePath(relative)callsPath.GetFullPathonPath + "/" + relative, thenEnsureWithinRoot. - On
net8.0,EnsureNoSymlinkEscaperesolves any symlink target and blocks it if the target is outside the root. -
LocalDirectoryenumeration skips any entry withFileAttributes.ReparsePoint(symlinks, junctions, mount points) on both TFMs.
CreateDirectory("a/b/c") and TryOpenDirectory("a/b/c", out _) split on / and \ via PathUtil.SplitAndValidateSegments, which validates every segment before any backend call. Absolute paths, ., and .. segments throw FileHubException.
| Driver | Sandbox? | Enforcement |
|---|---|---|
| Local | Yes |
ResolveSafePath + symlink filter (net8.0 also resolves link targets). |
| Memory | N/A | No ambient filesystem; traversal is impossible by construction. |
| OCI | Yes |
rootPath is a prefix; every object name is checked against it via PathUtil. |
var hub = new LocalFileHub($@"C:\tenants\{tenantId}");
// Every op below is confined to that tenant directory —
// user-supplied names can't escape even with ".." tricks.
hub.Root.CreateFile(userSuppliedName);Every entry has an IsReadOnly flag. When true, writes throw FileHubException before any I/O.
var ro = hub.Root.OpenDirectory("config").AsReadOnly();
ro.OpenFile("settings.json").ReadAllText(); // OK
ro.CreateFile("new.txt"); // FileHubException
ro.OpenFile("settings.json").SetText("x"); // FileHubExceptionOn a read-only FileEntry
|
On a read-only DirectoryEntry
|
|---|---|
GetWriteStream, SetText, SetBytes
|
CreateFile, CreateDirectory
|
Delete, Rename, MoveTo
|
Delete, Delete(name), DeleteIfExists
|
| — |
Rename, MoveTo, CopyTo
|
CopyTo on a file is allowed — it's a read on the source.
Anything the read-only wrapper returns is also read-only — you can't escape by navigating:
var ro = hub.Root.AsReadOnly();
ro.OpenFile("a.txt"); // read-only
ro.OpenDirectory("logs"); // read-only
ro.OpenFile("a.txt").Parent; // read-only
ro.GetFiles().First(); // read-onlyAsReadOnly() on an already read-only entry returns the same instance (no double wrap).
- Expose config/assets to callers without trusting them not to mutate.
- Hand a snapshot to plugin/unknown code.
- Enforce "pure read" semantics during an inspection pass.
void RunInspector(DirectoryEntry input) { /* plugin logic */ }
RunInspector(hub.Root.OpenDirectory("incoming").AsReadOnly());Object-storage drivers (S3, OCI) implement ISignedUploadable: a directory can mint a time-limited pre-signed PUT URL that a remote client uploads straight to, without the backend ever touching the bytes. The two providers differ in a security-relevant way when you pass FileWriteOptions (ContentType / CacheControl / Metadata):
| Provider | Options passed | Behavior |
|---|---|---|
| S3 | header-binding options | The options are baked into the pre-signed URL signature. The client's PUT must send matching Content-Type / Cache-Control / user-metadata headers, or S3 rejects it with a signature mismatch. This lets the backend constrain what the client is allowed to upload. |
| OCI | header-binding options | A pre-authenticated request (PAR) cannot bind request headers. The driver throws NotSupportedException rather than hand back an unconstrained URL the caller would wrongly believe is header-constrained. |
| OCI | no options / empty options | Returns a normal upload URL. |
The takeaway: do not assume header constraints carry across providers. On OCI, enforce required headers server-side after the upload completes. See Amazon S3 and Oracle Object Storage.
A 401 / 403 from a backend surfaces as UnauthorizedAccessException on every driver — S3 and OCI included, and the Local driver propagates the host OS's UnauthorizedAccessException unwrapped. Callers can catch that one type regardless of backend to detect a permissions failure.
- First statement of every write:
ThrowIfReadOnly(). - Validate every caller-supplied leaf name with
ValidateName. - Resolve nested paths through
NestedPath.TrySplitor the driver's own prefix combiner — never concatenate user input into the backend's path directly. - See Custom Drivers.
FileHub — unified file & directory API for .NET. Core is dependency-free; drivers are opt-in packages.