From 621af804e2abcef32b815ffbffd795c2b7ef0fe5 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 13:27:28 +0900 Subject: [PATCH] Fix TypeScript path alias config parsing #3033 --- changelog.d/unreleased/3033.security.md | 16 +++++ .../SymbolExtractor.TypeScriptPathAliases.cs | 15 ++++- tests/CodeIndex.Tests/SymbolExtractorTests.cs | 62 +++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 changelog.d/unreleased/3033.security.md diff --git a/changelog.d/unreleased/3033.security.md b/changelog.d/unreleased/3033.security.md new file mode 100644 index 0000000000..9b62882a14 --- /dev/null +++ b/changelog.d/unreleased/3033.security.md @@ -0,0 +1,16 @@ +--- +category: security +issues: + - 3033 +affected: + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.TypeScriptPathAliases.cs + - tests/CodeIndex.Tests/SymbolExtractorTests.cs +--- + +## English + +- **TypeScript path alias config parsing now reports malformed JSON and enforces a depth cap (#3033)** — `tsconfig.json` / `jsconfig.json` alias parsing emits the existing path-alias warning when JSON cannot be parsed and rejects excessively nested config JSON with a bounded `JsonDocument` depth. + +## 日本語 + +- **TypeScript path alias config の JSON parse 失敗を警告し depth 上限を適用するようになりました (#3033)** — `tsconfig.json` / `jsconfig.json` の alias 解析で JSON を parse できない場合は既存の path-alias warning を出し、過度にネストした config JSON は `JsonDocument` の depth 上限で拒否します。 diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.TypeScriptPathAliases.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.TypeScriptPathAliases.cs index 99d814f10f..16dc805524 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.TypeScriptPathAliases.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.TypeScriptPathAliases.cs @@ -8,8 +8,15 @@ public static partial class SymbolExtractor private const int MaxTypeScriptPathAliasConfigBytes = 256 * 1024; private const int MaxTypeScriptPathAliasTotalConfigBytes = 512 * 1024; private const int MaxTypeScriptPathAliasExtendsDepth = 8; + private const int MaxTypeScriptPathAliasConfigJsonDepth = 32; private static readonly object TypeScriptPathAliasWarningLock = new(); private static readonly HashSet TypeScriptPathAliasReportedWarnings = new(StringComparer.Ordinal); + private static readonly JsonDocumentOptions TypeScriptPathAliasConfigJsonOptions = new() + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip, + MaxDepth = MaxTypeScriptPathAliasConfigJsonDepth, + }; private sealed record TypeScriptPathAliasConfig(string ProjectDirectory, string BaseDirectory, bool HasBaseUrl, IReadOnlyList Rules); @@ -125,7 +132,13 @@ private static string ResolveJavaScriptTypeScriptModuleSpecifier(string lang, st document = JsonDocument.Parse( configText, - new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); + TypeScriptPathAliasConfigJsonOptions); + } + catch (JsonException) + { + ReportTypeScriptPathAliasWarningOnce( + $"Skipped TypeScript path alias config {configPath} because it could not be parsed as JSON within the {MaxTypeScriptPathAliasConfigJsonDepth}-level depth limit."); + return null; } catch { diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index 7fa9b8cd46..2b06172e62 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -18054,6 +18054,68 @@ public void Extract_TypeScript_OversizedTsconfigSkipsPathAliasesWithWarning() } } + [Fact] + public void Extract_TypeScript_MalformedTsconfigSkipsPathAliasesWithWarning() + { + var projectRoot = TestProjectHelper.CreateTempProject("tsconfig_alias_malformed_symbols"); + try + { + WriteFile(projectRoot, "tsconfig.json", """ + { + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } + """); + WriteFile(projectRoot, "src/components/Button.tsx", "export const Button = 1;\n"); + var sourcePath = WriteFile(projectRoot, "src/main.ts", "import { Button } from \"@/components/Button\";\n"); + + List symbols = []; + var stderr = ConsoleCapture.CaptureError(() => + symbols = SymbolExtractor.Extract(1, "typescript", File.ReadAllText(sourcePath), sourcePath)); + + Assert.Contains(symbols, s => s.Kind == "import" && s.Name == "@/components/Button"); + Assert.DoesNotContain(symbols, s => s.Kind == "import" && s.Name == "src/components/Button.tsx"); + Assert.Contains("Skipped TypeScript path alias config", stderr, StringComparison.Ordinal); + Assert.Contains("could not be parsed as JSON", stderr, StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void Extract_TypeScript_DeepTsconfigJsonSkipsPathAliasesWithWarning() + { + var projectRoot = TestProjectHelper.CreateTempProject("tsconfig_alias_deep_json_symbols"); + try + { + var deepJson = string.Concat(Enumerable.Repeat("{\"nested\":", 40)) + "0" + new string('}', 40); + WriteFile( + projectRoot, + "tsconfig.json", + "{\"compilerOptions\":{\"baseUrl\":\".\",\"paths\":{\"@/*\":[\"src/*\"]}},\"deep\":" + deepJson + "}"); + WriteFile(projectRoot, "src/components/Button.tsx", "export const Button = 1;\n"); + var sourcePath = WriteFile(projectRoot, "src/main.ts", "import { Button } from \"@/components/Button\";\n"); + + List symbols = []; + var stderr = ConsoleCapture.CaptureError(() => + symbols = SymbolExtractor.Extract(1, "typescript", File.ReadAllText(sourcePath), sourcePath)); + + Assert.Contains(symbols, s => s.Kind == "import" && s.Name == "@/components/Button"); + Assert.DoesNotContain(symbols, s => s.Kind == "import" && s.Name == "src/components/Button.tsx"); + Assert.Contains("Skipped TypeScript path alias config", stderr, StringComparison.Ordinal); + Assert.Contains("32-level depth limit", stderr, StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void Extract_TypeScript_ExcessiveTsconfigExtendsDepthSkipsInheritedPathAliasesWithWarning() {