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:
@@ -20,6 +20,9 @@ public static class NatsConfParser
|
||||
private const string BcryptPrefix2A = "2a$";
|
||||
private const string BcryptPrefix2B = "2b$";
|
||||
|
||||
// Maximum nesting depth for include directives to prevent infinite recursion.
|
||||
private const int MaxIncludeDepth = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Parses a NATS configuration string into a dictionary.
|
||||
/// </summary>
|
||||
@@ -34,12 +37,15 @@ public static class NatsConfParser
|
||||
/// <summary>
|
||||
/// Parses a NATS configuration file into a dictionary.
|
||||
/// </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 tokens = NatsConfLexer.Tokenize(data);
|
||||
var baseDir = Path.GetDirectoryName(Path.GetFullPath(filePath)) ?? string.Empty;
|
||||
var state = new ParserState(tokens, baseDir);
|
||||
var state = new ParserState(tokens, baseDir, [], includeDepth);
|
||||
state.Run();
|
||||
return state.Mapping;
|
||||
}
|
||||
@@ -57,7 +63,7 @@ public static class NatsConfParser
|
||||
var data = Encoding.UTF8.GetString(rawBytes);
|
||||
var tokens = NatsConfLexer.Tokenize(data);
|
||||
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();
|
||||
return (state.Mapping, digest);
|
||||
}
|
||||
@@ -66,11 +72,11 @@ public static class NatsConfParser
|
||||
/// 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.
|
||||
/// </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 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();
|
||||
return state.Mapping;
|
||||
}
|
||||
@@ -84,6 +90,7 @@ public static class NatsConfParser
|
||||
private readonly IReadOnlyList<Token> _tokens;
|
||||
private readonly string _baseDir;
|
||||
private readonly HashSet<string> _envVarReferences;
|
||||
private readonly int _includeDepth;
|
||||
private int _pos;
|
||||
|
||||
// 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 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;
|
||||
_baseDir = baseDir;
|
||||
_envVarReferences = envVarReferences;
|
||||
_includeDepth = includeDepth;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
@@ -149,9 +157,9 @@ public static class NatsConfParser
|
||||
|
||||
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];
|
||||
@@ -188,7 +196,10 @@ public static class NatsConfParser
|
||||
{
|
||||
var key = PopKey();
|
||||
map[key] = val;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"BUG in parser, unexpected context type {_ctx?.GetType().Name ?? "null"}");
|
||||
}
|
||||
|
||||
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
|
||||
// (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))
|
||||
{
|
||||
SetValue(parsedValue);
|
||||
@@ -391,8 +402,14 @@ public static class NatsConfParser
|
||||
/// </summary>
|
||||
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 includeResult = ParseFile(fullPath);
|
||||
var includeResult = ParseFile(fullPath, _includeDepth + 1);
|
||||
|
||||
foreach (var (key, value) in includeResult)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user