using ZB.MOM.WW.Configuration; using ZB.MOM.WW.Secrets.Configuration; namespace ZB.MOM.WW.Secrets.Tests.Configuration; /// /// Secrets-005: the store-path rules must compare locations, not spellings. A path that reaches /// the content root through a symbolic link (or a content root that is itself reached through one) /// is still inside it; on Windows a drive-relative path is not "absolute", and device-prefixed, /// loopback-admin-share and 8.3 spellings of a path inside the content root are still inside it. /// public sealed class SecretsStorePathSpellingTests : IDisposable { private readonly string _base; public SecretsStorePathSpellingTests() { _base = Path.Combine(Path.GetTempPath(), $"zb-secrets-spelling-{Guid.NewGuid():N}"); Directory.CreateDirectory(Path.Combine(_base, "app", "data")); Directory.CreateDirectory(Path.Combine(_base, "outside")); } public void Dispose() { try { Directory.Delete(_base, recursive: true); } catch (IOException) { } } private static bool Inside(string value, string root) { var builder = new ValidationBuilder(); SecretsStorePathRules.AddIfUnderContentRoot(value, root, "inside", builder); return !builder.IsValid; } private static bool NotRooted(string value) { var builder = new ValidationBuilder(); SecretsStorePathRules.AddIfNotRooted(value, "relative", builder); return !builder.IsValid; } private bool TryLink(string link, string target) { try { Directory.CreateSymbolicLink(link, target); return true; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { return false; // Windows without the symlink privilege } } [SkippableFact] public void A_store_reached_through_a_symlink_into_the_content_root_is_inside_it() { string root = Path.Combine(_base, "app"); string link = Path.Combine(_base, "alias"); Skip.IfNot(TryLink(link, root), "cannot create a symbolic link here"); // Positive control: the same file spelled directly is caught today. Assert.True(Inside(Path.Combine(root, "data", "secrets.db"), root)); Assert.True(Inside(Path.Combine(link, "data", "secrets.db"), root)); } [SkippableFact] public void A_content_root_reached_through_a_current_symlink_still_contains_its_store() { // The "current -> releases/42" deployment shape: the host reports the link, the store is // configured with the real directory (or the other way round). string real = Path.Combine(_base, "app"); string current = Path.Combine(_base, "current"); Skip.IfNot(TryLink(current, real), "cannot create a symbolic link here"); Assert.True(Inside(Path.Combine(real, "data", "secrets.db"), current)); Assert.True(Inside(Path.Combine(current, "data", "secrets.db"), real)); } [SkippableFact] public void A_store_spelled_through_the_content_root_stays_refused_even_if_a_link_leads_out() { // A "durable" link inside the app directory pointing at storage elsewhere: the bytes survive // a rename, but the next deploy's tree has no such link, so the configured spelling then // names a fresh empty store inside the new tree — the 2026-08-09 shape. Resolving links // must ADD cases, never remove the spelled one. string root = Path.Combine(_base, "app"); string link = Path.Combine(root, "durable"); Skip.IfNot(TryLink(link, Path.Combine(_base, "outside")), "cannot create a symbolic link here"); Assert.True(Inside(Path.Combine(link, "secrets.db"), root)); Assert.False(Inside(Path.Combine(_base, "outside", "secrets.db"), root)); // control } [Fact] public void A_not_yet_created_store_is_compared_as_written() { string root = Path.Combine(_base, "app"); Assert.True(Inside(Path.Combine(root, "new", "deeper", "secrets.db"), root)); Assert.False(Inside(Path.Combine(_base, "outside", "new", "secrets.db"), root)); } [SkippableFact] public void Drive_and_root_relative_paths_are_not_absolute_on_windows() { Skip.IfNot(OperatingSystem.IsWindows(), "Windows path semantics"); Assert.True(NotRooted(@"C:secrets.db")); Assert.True(NotRooted(@"\secrets.db")); Assert.False(NotRooted(@"C:\data\secrets.db")); Assert.False(NotRooted(@"\\server\share\secrets.db")); } [SkippableFact] public void Device_prefixed_and_loopback_share_spellings_are_inside_on_windows() { Skip.IfNot(OperatingSystem.IsWindows(), "Windows path semantics"); string root = Path.Combine(_base, "app"); string full = Path.GetFullPath(Path.Combine(root, "data", "secrets.db")); string drive = full[..1]; string rest = full[3..]; Assert.True(Inside(@"\\?\" + full, root)); Assert.True(Inside(@"\\.\" + full, root)); Assert.True(Inside($@"\\localhost\{drive}$\{rest}", root)); Assert.True(Inside($@"\\127.0.0.1\{drive}$\{rest}", root)); Assert.True(Inside($@"\\?\UNC\localhost\{drive}$\{rest}", root)); } [SkippableFact] public void An_8_3_short_name_spelling_is_inside_on_windows() { Skip.IfNot(OperatingSystem.IsWindows(), "Windows path semantics"); string longDir = Path.Combine(_base, "Application Directory"); Directory.CreateDirectory(longDir); string? shortDir = ShortName(longDir); Skip.If(shortDir is null || !shortDir.Contains('~'), "8.3 names are disabled on this volume"); Assert.True(Inside(Path.Combine(shortDir!, "secrets.db"), longDir)); Assert.True(Inside(Path.Combine(longDir, "secrets.db"), shortDir!)); } [SkippableFact] public void An_ordinary_local_share_pointing_at_the_content_root_is_inside_on_windows() { Skip.IfNot(OperatingSystem.IsWindows(), "Windows path semantics"); string root = Path.Combine(_base, "app"); string share = "zbtest" + Guid.NewGuid().ToString("N")[..8]; Skip.IfNot(CreateShare(share, root), "cannot create an SMB share here (needs Administrators)"); try { Assert.True(Inside($@"\\localhost\{share}\data\secrets.db", root)); Assert.True(Inside($@"\\{Environment.MachineName}\{share}\secrets.db", root)); Assert.False(Inside($@"\\localhost\{share}\secrets.db", Path.Combine(_base, "outside"))); } finally { _ = NetShareDel(null, share, 0); } } private static bool CreateShare(string name, string path) { var info = new ShareInfo2 { shi2_netname = name, shi2_type = 0, shi2_max_uses = uint.MaxValue, shi2_path = path }; return NetShareAdd(null, 2, ref info, out _) == 0; } [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Unicode)] private struct ShareInfo2 { public string shi2_netname; public uint shi2_type; public string? shi2_remark; public uint shi2_permissions; public uint shi2_max_uses; public uint shi2_current_uses; public string shi2_path; public string? shi2_passwd; } [System.Runtime.InteropServices.DllImport("netapi32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] private static extern uint NetShareAdd(string? server, uint level, ref ShareInfo2 buf, out uint parmErr); [System.Runtime.InteropServices.DllImport("netapi32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] private static extern uint NetShareDel(string? server, string netName, uint reserved); private static string? ShortName(string path) { char[] buffer = new char[1024]; uint n = GetShortPathNameW(path, buffer, (uint)buffer.Length); return n == 0 ? null : new string(buffer, 0, (int)n); } [System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] private static extern uint GetShortPathNameW(string longPath, [System.Runtime.InteropServices.Out] char[] shortPath, uint size); }