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
26 changes: 26 additions & 0 deletions changelog.d/unreleased/3053.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
category: security
issues:
- 3053
affected:
- src/CodeIndex/Indexer/BoundedRegex.cs
- src/CodeIndex/Indexer/References/ReferenceExtractor.cs
- src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs
- src/CodeIndex/Indexer/References/Languages/CssReferenceExtractor.cs
- src/CodeIndex/Indexer/References/Languages/DartReferenceExtractor.cs
- src/CodeIndex/Indexer/References/Languages/JavaReferenceExtractor.cs
- src/CodeIndex/Indexer/References/Languages/CobolReferenceExtractor.cs
- src/CodeIndex/Indexer/References/Languages/PerlReferenceExtractor.cs
- src/CodeIndex/Indexer/References/Languages/SqlReferenceExtractor.cs
- src/CodeIndex/Indexer/References/Languages/TerraformReferenceExtractor.cs
- tests/CodeIndex.Tests/BoundedRegexTests.cs
- tests/CodeIndex.Tests/ReferenceExtractorCssTests.cs
---

## English

- **Built-in reference extractor regex enumeration is timeout-bounded (#3053)** — shared match enumeration now forces and catches regex timeouts before built-in extractors iterate matches from repository-controlled source text.

## 日本語

- **組み込み reference extractor の正規表現列挙をタイムアウト付きにしました (#3053)** — リポジトリ由来のソース文字列から組み込み extractor が match を列挙する前に、共有の列挙処理で正規表現タイムアウトを強制・捕捉するようにしました。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3054.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3054
affected:
- src/CodeIndex/Indexer/References/Languages/TypeScriptReferenceExtractor.cs
- tests/CodeIndex.Tests/ReferenceExtractorTests.cs
---

## English

- **TypeScript namespace alias references no longer build per-line dynamic regexes (#3054)** — namespace alias qualified usages are now found with a bounded scanner that preserves identifier boundaries without compiling one regex per alias per line.

## 日本語

- **TypeScript namespace alias 参照で行ごとの動的正規表現を生成しないようにしました (#3054)** — namespace alias の qualified usage は、alias ごと・行ごとの regex を組み立てず、識別子境界を保つ bounded scanner で検出するようになりました。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3101.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3101
affected:
- src/CodeIndex/Database/DbWriter.cs
- tests/CodeIndex.Tests/DatabaseTests.cs
---

## English

- **C# import alias registration now uses timeout-bounded signature regexes (#3101)** — `DbWriter` no longer calls the BCL regex APIs directly when classifying C# `using` signatures from indexed content.

## 日本語

- **C# import alias 登録で timeout 付き signature regex を使うようにしました (#3101)** — `DbWriter` は indexed content 由来の C# `using` signature を分類するとき、BCL の regex API を直接呼び出さなくなりました。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3141.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3141
affected:
- src/CodeIndex/Indexer/References/ReferenceExtractor.cs
- tests/CodeIndex.Tests/ReferenceExtractorPythonTests.cs
---

## English

- **Python logical reference remapping now caps header and statement maps (#3141)** — oversized multiline Python headers or continuation statements now skip logical remapping instead of growing unbounded text and line/column arrays.

## 日本語

- **Python logical reference remap の header / statement map に上限を設けました (#3141)** — 巨大な複数行 header や continuation statement では、text と line/column 配列を無制限に増やさず logical remap をスキップします。
23 changes: 17 additions & 6 deletions src/CodeIndex/Database/DbWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using CodeIndex.Indexer;
using CodeIndex.Models;
using System.Text;
using System.Text.RegularExpressions;

namespace CodeIndex.Database;

Expand Down Expand Up @@ -35,6 +36,18 @@ public class DbWriter
private const int DeleteFilesBatchSize = 500;
private const int MaxSqlVariables = 999;
private const int SqliteConstraintErrorCode = 19;
private static readonly BoundedRegex CSharpExternAliasSignatureRegex = new(
@"^\s*extern\s+alias\b",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly BoundedRegex CSharpGlobalUsingSignatureRegex = new(
@"^\s*global\s+using\b",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly BoundedRegex CSharpUsingStaticSignatureRegex = new(
@"^\s*(?:global\s+)?using\s+static\b",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly BoundedRegex CSharpUsingAliasSignatureRegex = new(
@"^\s*(?:global\s+)?using\s+(?<alias>@?\w+)\s*=\s*(?<target>[^;]+?)\s*;",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private int _rowSkipSavepointCounter;
private long _batchRowsSkipped;
private int _transactionDepth;
Expand Down Expand Up @@ -2361,14 +2374,14 @@ private static void RegisterCSharpImport(FileImportSet perFile, FileImportSet gl
// `extern alias X;` も import 行として現れるがアセンブリ別名でしかなく resolver 側の
// qualified 索引には載らないので対象外。
if (signature != null && signature.IndexOf("extern", StringComparison.Ordinal) >= 0
&& System.Text.RegularExpressions.Regex.IsMatch(signature, @"^\s*extern\s+alias\b"))
&& CSharpExternAliasSignatureRegex.IsMatch(signature))
{
return;
}
bool isGlobal = signature != null
&& System.Text.RegularExpressions.Regex.IsMatch(signature, @"^\s*global\s+using\b");
&& CSharpGlobalUsingSignatureRegex.IsMatch(signature);
bool isStatic = signature != null
&& System.Text.RegularExpressions.Regex.IsMatch(signature, @"^\s*(?:global\s+)?using\s+static\b");
&& CSharpUsingStaticSignatureRegex.IsMatch(signature);
// `using static Foo.Bar;` imports the static members of `Foo.Bar` into the file's
// scope — NOT a namespace that a base clause `class X : Base` could pull from.
// Drop it so we don't confuse the alias/namespace paths.
Expand All @@ -2384,9 +2397,7 @@ private static void RegisterCSharpImport(FileImportSet perFile, FileImportSet gl
// the alias enters the per-file map.
// SymbolExtractor 側と同じく verbatim 識別子も `@?\w+` で受け、下の正規化で
// 先頭 `@` を剥がしてから alias map に載せる。
var m = System.Text.RegularExpressions.Regex.Match(
signature,
@"^\s*(?:global\s+)?using\s+(?<alias>@?\w+)\s*=\s*(?<target>[^;]+?)\s*;");
var m = CSharpUsingAliasSignatureRegex.Match(signature);
if (m.Success)
{
aliasName = m.Groups["alias"].Value.Trim();
Expand Down
49 changes: 49 additions & 0 deletions src/CodeIndex/Indexer/BoundedRegex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ public BoundedRegex(string pattern, RegexOptions options, TimeSpan matchTimeout)
}
}

public static BclMatch Match(BclRegex regex, string input)
{
try
{
return regex.Match(input);
}
catch (RegexMatchTimeoutException)
{
return BclMatch.Empty;
}
}

public static new MatchCollection Matches(string input, string pattern) =>
Matches(input, pattern, RegexOptions.None);

Expand All @@ -60,6 +72,43 @@ public BoundedRegex(string pattern, RegexOptions options, TimeSpan matchTimeout)
}
}

public static IEnumerable<BclMatch> EnumerateMatches(BclRegex regex, string input)
{
MatchCollection matches;
try
{
matches = regex.Matches(input);
_ = matches.Count;
}
catch (RegexMatchTimeoutException)
{
yield break;
}

foreach (BclMatch match in matches)
yield return match;
}

public static IEnumerable<BclMatch> EnumerateMatches(string input, string pattern) =>
EnumerateMatches(input, pattern, RegexOptions.None);

public static IEnumerable<BclMatch> EnumerateMatches(string input, string pattern, RegexOptions options)
{
MatchCollection matches;
try
{
matches = BclRegex.Matches(input, pattern, options, DefaultMatchTimeout);
_ = matches.Count;
}
catch (RegexMatchTimeoutException)
{
yield break;
}

foreach (BclMatch match in matches)
yield return match;
}

public static new bool IsMatch(string input, string pattern) =>
IsMatch(input, pattern, RegexOptions.None);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public static void Emit(
foreach (var pattern in StatementPatterns)
EmitMatches(pattern, rawLine, references, seen, fileId, context, lineNumber, container);

foreach (Match match in CobolPerformRegex.Matches(rawLine))
foreach (Match match in BoundedRegex.EnumerateMatches(CobolPerformRegex, rawLine))
{
var endName = match.Groups["end"].Value;
if (!string.IsNullOrWhiteSpace(endName)
Expand Down Expand Up @@ -197,7 +197,7 @@ private static void EmitMatches(
int lineNumber,
SymbolRecord? container)
{
foreach (Match match in pattern.Regex.Matches(rawLine))
foreach (Match match in BoundedRegex.EnumerateMatches(pattern.Regex, rawLine))
EmitNamedReference(references, seen, fileId, match, pattern.ReferenceKind, context, lineNumber, container);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ private static void EmitMatches(
HashSet<string>? definitionNames,
SymbolRecord? container)
{
foreach (Match match in pattern.Regex.Matches(preparedLine))
foreach (Match match in BoundedRegex.EnumerateMatches(pattern.Regex, preparedLine))
{
var nameGroup = match.Groups["name"];
if (definitionNames != null && definitionNames.Contains(nameGroup.Value))
Expand Down Expand Up @@ -443,7 +443,7 @@ private static void EmitCssSelectorMatches(
HashSet<string>? definitionNames,
SymbolRecord? container)
{
foreach (Match match in regex.Matches(selectorPartBody))
foreach (Match match in BoundedRegex.EnumerateMatches(regex, selectorPartBody))
{
var nameGroup = match.Groups["name"];
var prefixIndex = nameGroup.Index - 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private static void EmitSpecialReferences(

void EmitSingleMatch(Regex regex, string referenceKind)
{
var match = regex.Match(preparedLine);
var match = BoundedRegex.Match(regex, preparedLine);
if (!match.Success)
return;

Expand All @@ -76,13 +76,13 @@ void EmitMixinReferences()
return;

var names = match.Groups["names"];
foreach (Match name in Regex.Matches(names.Value, @"[A-Za-z_]\w*"))
foreach (Match name in BoundedRegex.EnumerateMatches(names.Value, @"[A-Za-z_]\w*"))
Add(name.Value, names.Index + name.Index, "mixin_in");
}

void EmitNamedConstructorCalls()
{
foreach (Match match in NamedConstructorCallRegex.Matches(preparedLine))
foreach (Match match in BoundedRegex.EnumerateMatches(NamedConstructorCallRegex, preparedLine))
{
var name = match.Groups["name"];
Add(name.Value, name.Index, "named_ctor_call");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ public static void EmitModuleDirectiveReferences(
lineNumber,
resolveContainerForColumn);

foreach (Match match in ModuleProvidesDirectiveReferenceRegex.Matches(preparedLine))
foreach (Match match in BoundedRegex.EnumerateMatches(ModuleProvidesDirectiveReferenceRegex, preparedLine))
{
var serviceGroup = match.Groups["service"];
ReferenceExtractor.AddTypeReferenceSegment(
Expand Down Expand Up @@ -1185,7 +1185,7 @@ private static void EmitModuleDirectiveReference(
int lineNumber,
Func<int, SymbolRecord?> resolveContainerForColumn)
{
foreach (Match match in regex.Matches(preparedLine))
foreach (Match match in BoundedRegex.EnumerateMatches(regex, preparedLine))
{
var nameGroup = match.Groups["name"];
ReferenceExtractor.AddTypeReferenceSegment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ private static void AddQuotedModuleReferences(
{
var args = argsGroup.Value;
var argsStart = argsGroup.Index;
foreach (Match moduleMatch in QuotedModuleRegex.Matches(args))
foreach (Match moduleMatch in BoundedRegex.EnumerateMatches(QuotedModuleRegex, args))
{
if (moduleMatch.Groups["name"].Success)
{
Expand All @@ -204,7 +204,7 @@ private static void AddQuotedModuleReferences(

var names = namesGroup.Value;
var namesStart = argsStart + namesGroup.Index;
foreach (Match nameMatch in Regex.Matches(names, @"[\p{L}_][\w:]*", RegexOptions.CultureInvariant))
foreach (Match nameMatch in BoundedRegex.EnumerateMatches(names, @"[\p{L}_][\w:]*", RegexOptions.CultureInvariant))
AddBaseModuleReference(nameMatch.Value, namesStart + nameMatch.Index, references, seen, fileId, context, lineNumber, resolveContainerForCall);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2104,7 +2104,7 @@ private static void EmitQualifiedColumnReferences(
Func<string, bool> shouldIgnoreName,
string referenceKind)
{
foreach (Match match in QualifiedColumnReferenceRegex.Matches(text))
foreach (Match match in BoundedRegex.EnumerateMatches(QualifiedColumnReferenceRegex, text))
{
if (IsInsideDoubleQuotedRegion(text, match.Index))
continue;
Expand Down Expand Up @@ -2159,7 +2159,7 @@ private static void EmitMergeColumnReference(
rawIndex += leafIndex;
rawName = rawName[leafIndex..].TrimStart();

var match = Regex.Match(
var match = BoundedRegex.Match(
rawName,
$"^(?<name>{QuotedIdentifierPattern}|{BareIdentifierPattern})",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
Expand Down Expand Up @@ -3121,7 +3121,7 @@ private static bool TryFindDefinitionLeafSpan(string line, string qualifiedName,
pattern.Append(escaped);
}

var match = Regex.Match(line, pattern.ToString(), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
var match = BoundedRegex.Match(line, pattern.ToString(), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (!match.Success)
return false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private static void EmitMatches(
HashSet<string>? definitionNames,
SymbolRecord? container)
{
foreach (Match match in pattern.Regex.Matches(preparedLine))
foreach (Match match in BoundedRegex.EnumerateMatches(pattern.Regex, preparedLine))
{
var nameGroup = match.Groups["name"];
if (definitionNames != null && definitionNames.Contains(nameGroup.Value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1141,24 +1141,57 @@ private static void EmitNamespaceAliasQualifiedReferences(
continue;
}

foreach (Match match in Regex.Matches(
preparedLine,
$@"(?<![\w$]){Regex.Escape(binding.Alias)}\s*\.\s*[A-Za-z_$][\w$]*"))
foreach (var matchIndex in EnumerateNamespaceAliasQualifiedReferenceStarts(preparedLine, binding.Alias))
{
ReferenceExtractor.AddReference(
references,
seen,
fileId,
binding.ModuleSpecifier,
match.Index,
matchIndex,
"reference",
context,
lineNumber,
resolveContainerForColumn(match.Index));
resolveContainerForColumn(matchIndex));
}
}
}

private static IEnumerable<int> EnumerateNamespaceAliasQualifiedReferenceStarts(string text, string alias)
{
if (string.IsNullOrEmpty(alias))
yield break;

var searchIndex = 0;
while (searchIndex < text.Length)
{
var aliasIndex = text.IndexOf(alias, searchIndex, StringComparison.Ordinal);
if (aliasIndex < 0)
yield break;

searchIndex = aliasIndex + Math.Max(1, alias.Length);
if (aliasIndex > 0 && IsTypeScriptIdentifierPart(text[aliasIndex - 1]))
continue;

var afterAlias = aliasIndex + alias.Length;
if (afterAlias < text.Length && IsTypeScriptIdentifierPart(text[afterAlias]))
continue;

var dotIndex = SkipWhitespace(text, afterAlias);
if (dotIndex >= text.Length || text[dotIndex] != '.')
continue;

var memberIndex = SkipWhitespace(text, dotIndex + 1);
if (memberIndex >= text.Length || !IsTypeScriptNamespaceMemberStart(text[memberIndex]))
continue;

yield return aliasIndex;
}
}

private static bool IsTypeScriptNamespaceMemberStart(char ch) =>
ch == '_' || ch == '$' || ch is >= 'A' and <= 'Z' || ch is >= 'a' and <= 'z';

private static int? FindShadowLine(IReadOnlyList<string> preparedLines, string alias, int bindingLine)
{
for (var index = bindingLine; index < preparedLines.Count; index++)
Expand Down
Loading
Loading