Fensalir/src/Aquarium.Engine/Runtime/TrustedRootSetting.cs
Aqua 467a97f540 Trusted root: lexical check before any filesystem access, cached env setting, stricter log escaping
Review (Heimdall): a path outside the pinned root (as resolved or as given) is refused by
string comparison before File.Exists or any link is followed; the trusted-root setting is
its own small type that takes the environment variable once, caches it and refuses blank
values; Printable also escapes backslash, format characters (bidi, zero-width) and the
line and paragraph separators; dev-watch matches the host ack by the unique slot leaf with
doubled backslashes; dev-reload prunes live-slots and rejects stale reopen state up front.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 11:04:00 +00:00

48 lines
1.9 KiB
C#

namespace Aquarium.Engine.Runtime;
/// <summary>
/// The client runtime trusted root as given on the command line or in AQUARIUM_CLIENT_TRUSTED_ROOT. The environment
/// value is taken (read and cleared) the first time anything is resolved, on every path, and cached: children do not
/// inherit it and a second resolution does not silently fall back to the default root. A blank flag value, a flag with
/// no value or a blank environment value is refused.
/// </summary>
internal sealed class TrustedRootSetting(Func<string?> takeEnvironment)
{
private const string Flag = "--client-trusted-root";
private const string Variable = "AQUARIUM_CLIENT_TRUSTED_ROOT";
private readonly Lazy<string?> fromEnvironment = new(takeEnvironment);
public static TrustedRootSetting FromProcessEnvironment() => new(() =>
{
var value = Environment.GetEnvironmentVariable(Variable);
Environment.SetEnvironmentVariable(Variable, null);
return value;
});
public string? Resolve(IReadOnlyCollection<string> args)
{
var environment = fromEnvironment.Value;
if (environment is not null && string.IsNullOrWhiteSpace(environment))
{
throw new ArgumentException($"{Variable} is set but empty; an empty value would fall back to the default root.");
}
var values = args.ToArray();
for (var index = 0; index < values.Length; index++)
{
if (!string.Equals(values[index], Flag, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (index + 1 >= values.Length || string.IsNullOrWhiteSpace(values[index + 1]))
{
throw new ArgumentException($"{Flag} needs a directory; an empty value would fall back to the default root.");
}
return values[index + 1];
}
return environment;
}
}