fix: add include depth limit, fix PopContext guard, add SetValue fallback

- Add MaxIncludeDepth = 10 constant and thread _includeDepth through ParserState
  constructors, ProcessInclude, ParseFile (private overload), and ParseEnvValue
  to prevent StackOverflowException from recursive includes
- Fix PopContext to check _ctxs.Count <= 1 instead of == 0 so the root context
  is never popped, replacing silent crash with clear InvalidOperationException
- Add else throw in SetValue so unknown context types surface as bugs rather
  than silently dropping values
This commit is contained in:
Joseph Doherty
2026-02-23 04:42:37 -05:00
parent 9f66ef72c6
commit 5219f77f9b

View File

@@ -20,6 +20,9 @@ public static class NatsConfParser
private const string BcryptPrefix2A = "2a$"; private const string BcryptPrefix2A = "2a$";
private const string BcryptPrefix2B = "2b$"; private const string BcryptPrefix2B = "2b$";
// Maximum nesting depth for include directives to prevent infinite recursion.
private const int MaxIncludeDepth = 10;
/// <summary> /// <summary>
/// Parses a NATS configuration string into a dictionary. /// Parses a NATS configuration string into a dictionary.
/// </summary> /// </summary>
@@ -34,12 +37,15 @@ public static class NatsConfParser
/// <summary> /// <summary>
/// Parses a NATS configuration file into a dictionary. /// Parses a NATS configuration file into a dictionary.
/// </summary> /// </summary>
public static Dictionary<string, object?> ParseFile(string filePath) public static Dictionary<string, object?> ParseFile(string filePath) =>
ParseFile(filePath, includeDepth: 0);
private static Dictionary<string, object?> ParseFile(string filePath, int includeDepth)
{ {
var data = File.ReadAllText(filePath); var data = File.ReadAllText(filePath);
var tokens = NatsConfLexer.Tokenize(data); var tokens = NatsConfLexer.Tokenize(data);
var baseDir = Path.GetDirectoryName(Path.GetFullPath(filePath)) ?? string.Empty; var baseDir = Path.GetDirectoryName(Path.GetFullPath(filePath)) ?? string.Empty;
var state = new ParserState(tokens, baseDir); var state = new ParserState(tokens, baseDir, [], includeDepth);
state.Run(); state.Run();
return state.Mapping; return state.Mapping;
} }
@@ -57,7 +63,7 @@ public static class NatsConfParser
var data = Encoding.UTF8.GetString(rawBytes); var data = Encoding.UTF8.GetString(rawBytes);
var tokens = NatsConfLexer.Tokenize(data); var tokens = NatsConfLexer.Tokenize(data);
var baseDir = Path.GetDirectoryName(Path.GetFullPath(filePath)) ?? string.Empty; var baseDir = Path.GetDirectoryName(Path.GetFullPath(filePath)) ?? string.Empty;
var state = new ParserState(tokens, baseDir); var state = new ParserState(tokens, baseDir, [], includeDepth: 0);
state.Run(); state.Run();
return (state.Mapping, digest); return (state.Mapping, digest);
} }
@@ -66,11 +72,11 @@ public static class NatsConfParser
/// Internal: parse an environment variable value by wrapping it in a synthetic /// Internal: parse an environment variable value by wrapping it in a synthetic
/// key-value assignment and parsing it. Shares the parent's env var cycle tracker. /// key-value assignment and parsing it. Shares the parent's env var cycle tracker.
/// </summary> /// </summary>
private static Dictionary<string, object?> ParseEnvValue(string value, HashSet<string> envVarReferences) private static Dictionary<string, object?> ParseEnvValue(string value, HashSet<string> envVarReferences, int includeDepth)
{ {
var synthetic = $"pk={value}"; var synthetic = $"pk={value}";
var tokens = NatsConfLexer.Tokenize(synthetic); var tokens = NatsConfLexer.Tokenize(synthetic);
var state = new ParserState(tokens, baseDir: string.Empty, envVarReferences); var state = new ParserState(tokens, baseDir: string.Empty, envVarReferences, includeDepth);
state.Run(); state.Run();
return state.Mapping; return state.Mapping;
} }
@@ -84,6 +90,7 @@ public static class NatsConfParser
private readonly IReadOnlyList<Token> _tokens; private readonly IReadOnlyList<Token> _tokens;
private readonly string _baseDir; private readonly string _baseDir;
private readonly HashSet<string> _envVarReferences; private readonly HashSet<string> _envVarReferences;
private readonly int _includeDepth;
private int _pos; private int _pos;
// The context stack holds either Dictionary<string, object?> (map) or List<object?> (array). // The context stack holds either Dictionary<string, object?> (map) or List<object?> (array).
@@ -96,15 +103,16 @@ public static class NatsConfParser
public Dictionary<string, object?> Mapping { get; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary<string, object?> Mapping { get; } = new(StringComparer.OrdinalIgnoreCase);
public ParserState(IReadOnlyList<Token> tokens, string baseDir) public ParserState(IReadOnlyList<Token> tokens, string baseDir)
: this(tokens, baseDir, []) : this(tokens, baseDir, [], includeDepth: 0)
{ {
} }
public ParserState(IReadOnlyList<Token> tokens, string baseDir, HashSet<string> envVarReferences) public ParserState(IReadOnlyList<Token> tokens, string baseDir, HashSet<string> envVarReferences, int includeDepth)
{ {
_tokens = tokens; _tokens = tokens;
_baseDir = baseDir; _baseDir = baseDir;
_envVarReferences = envVarReferences; _envVarReferences = envVarReferences;
_includeDepth = includeDepth;
} }
public void Run() public void Run()
@@ -149,9 +157,9 @@ public static class NatsConfParser
private object PopContext() private object PopContext()
{ {
if (_ctxs.Count == 0) if (_ctxs.Count <= 1)
{ {
throw new InvalidOperationException("BUG in parser, context stack empty"); throw new InvalidOperationException("BUG in parser, context stack underflow");
} }
var last = _ctxs[^1]; var last = _ctxs[^1];
@@ -188,7 +196,10 @@ public static class NatsConfParser
{ {
var key = PopKey(); var key = PopKey();
map[key] = val; map[key] = val;
return;
} }
throw new InvalidOperationException($"BUG in parser, unexpected context type {_ctx?.GetType().Name ?? "null"}");
} }
private void ProcessItem(Token token) private void ProcessItem(Token token)
@@ -367,7 +378,7 @@ public static class NatsConfParser
{ {
// Parse the env value through the full parser to get correct typing // Parse the env value through the full parser to get correct typing
// (e.g., "42" becomes long 42, "true" becomes bool, etc.). // (e.g., "42" becomes long 42, "true" becomes bool, etc.).
var subResult = ParseEnvValue(envValue, _envVarReferences); var subResult = ParseEnvValue(envValue, _envVarReferences, _includeDepth);
if (subResult.TryGetValue("pk", out var parsedValue)) if (subResult.TryGetValue("pk", out var parsedValue))
{ {
SetValue(parsedValue); SetValue(parsedValue);
@@ -391,8 +402,14 @@ public static class NatsConfParser
/// </summary> /// </summary>
private void ProcessInclude(string includePath) private void ProcessInclude(string includePath)
{ {
if (_includeDepth >= MaxIncludeDepth)
{
throw new FormatException(
$"Include depth limit of {MaxIncludeDepth} exceeded while processing '{includePath}'");
}
var fullPath = Path.Combine(_baseDir, includePath); var fullPath = Path.Combine(_baseDir, includePath);
var includeResult = ParseFile(fullPath); var includeResult = ParseFile(fullPath, _includeDepth + 1);
foreach (var (key, value) in includeResult) foreach (var (key, value) in includeResult)
{ {