diff --git a/src/NATS.Server/Configuration/NatsConfParser.cs b/src/NATS.Server/Configuration/NatsConfParser.cs index 77fa52a..e948bb8 100644 --- a/src/NATS.Server/Configuration/NatsConfParser.cs +++ b/src/NATS.Server/Configuration/NatsConfParser.cs @@ -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; + /// /// Parses a NATS configuration string into a dictionary. /// @@ -34,12 +37,15 @@ public static class NatsConfParser /// /// Parses a NATS configuration file into a dictionary. /// - public static Dictionary ParseFile(string filePath) + public static Dictionary ParseFile(string filePath) => + ParseFile(filePath, includeDepth: 0); + + private static Dictionary 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. /// - private static Dictionary ParseEnvValue(string value, HashSet envVarReferences) + private static Dictionary ParseEnvValue(string value, HashSet 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 _tokens; private readonly string _baseDir; private readonly HashSet _envVarReferences; + private readonly int _includeDepth; private int _pos; // The context stack holds either Dictionary (map) or List (array). @@ -96,15 +103,16 @@ public static class NatsConfParser public Dictionary Mapping { get; } = new(StringComparer.OrdinalIgnoreCase); public ParserState(IReadOnlyList tokens, string baseDir) - : this(tokens, baseDir, []) + : this(tokens, baseDir, [], includeDepth: 0) { } - public ParserState(IReadOnlyList tokens, string baseDir, HashSet envVarReferences) + public ParserState(IReadOnlyList tokens, string baseDir, HashSet 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 /// 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) {