fix(secrets): harden SecretName (reject rooted paths, fail-fast on default) + keep SecretDecryptionException sealed

This commit is contained in:
Joseph Doherty
2026-07-15 16:37:21 -04:00
parent b784c7117f
commit 01911508b9
2 changed files with 36 additions and 4 deletions
@@ -16,8 +16,9 @@ public readonly record struct SecretName
/// <param name="value">The raw name to normalize and validate.</param>
/// <exception cref="ArgumentException">
/// The value is null, empty, or whitespace; contains the path-traversal sequence
/// <c>..</c>; or contains any character outside <c>[a-z0-9._/-]</c> (after trim and
/// lowercasing).
/// <c>..</c>; is rooted (starts or ends with <c>/</c>) or contains an empty path
/// segment (<c>//</c>); or contains any character outside <c>[a-z0-9._/-]</c> (after
/// trim and lowercasing).
/// </exception>
public SecretName(string value)
{
@@ -37,6 +38,14 @@ public readonly record struct SecretName
nameof(value));
}
if (normalized.StartsWith('/') || normalized.EndsWith('/') ||
normalized.Contains("//", StringComparison.Ordinal))
{
throw new ArgumentException(
$"Secret name must not be rooted or contain an empty path segment: it may not start or end with '/' or contain '//' (was '{value}').",
nameof(value));
}
foreach (char c in normalized)
{
if (!AllowedChars.Contains(c, StringComparison.Ordinal))
@@ -47,17 +56,22 @@ public readonly record struct SecretName
}
}
Value = normalized;
_value = normalized;
}
private readonly string? _value;
/// <summary>The normalized secret name (trimmed, lowercased, allow-list validated).</summary>
public string Value { get; }
/// <exception cref="InvalidOperationException">The instance is the uninitialized <c>default</c> struct value.</exception>
public string Value => _value ?? throw new InvalidOperationException("SecretName is uninitialized (default struct value).");
/// <summary>Returns the normalized <see cref="Value"/>.</summary>
/// <returns>The normalized secret name.</returns>
/// <exception cref="InvalidOperationException">The instance is the uninitialized <c>default</c> struct value.</exception>
public override string ToString() => Value;
/// <summary>Implicitly converts a <see cref="SecretName"/> to its underlying string value.</summary>
/// <param name="name">The secret name to convert.</param>
/// <exception cref="InvalidOperationException">The instance is the uninitialized <c>default</c> struct value.</exception>
public static implicit operator string(SecretName name) => name.Value;
}