Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3033.security.md
Original file line number Diff line number Diff line change
@@ -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 上限で拒否します。
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> 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<TypeScriptPathAliasRule> Rules);

Expand Down Expand Up @@ -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
{
Expand Down
62 changes: 62 additions & 0 deletions tests/CodeIndex.Tests/SymbolExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SymbolRecord> 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<SymbolRecord> 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()
{
Expand Down
Loading