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 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)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user