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
20 changes: 20 additions & 0 deletions changelog.d/unreleased/3220.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
category: security
issues:
- 3220
affected:
- src/CodeIndex/Cli/DbPathResolver.cs
- src/CodeIndex/Cli/DbCommandRunner.cs
- src/CodeIndex/Cli/DiffCommandRunner.cs
- src/CodeIndex/Database/DbContext.cs
- tests/CodeIndex.Tests/DbPathResolverTests.cs
- tests/CodeIndex.Tests/DbCommandRunnerTests.cs
---

## English

- **SQLite file URI database opens now avoid connection-string injection (#3220)** — user-supplied `file:` URI database paths are built with `SqliteConnectionStringBuilder`, so `;Mode=...` payloads stay inside the data source value instead of becoming connection options.

## 日本語

- **SQLite file URI の DB open で connection-string injection を防止しました (#3220)** — ユーザー指定の `file:` URI DB パスは `SqliteConnectionStringBuilder` で組み立て、`;Mode=...` payload が接続オプションではなく data source 値に留まるようにしました。
18 changes: 4 additions & 14 deletions src/CodeIndex/Cli/DbCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -422,13 +422,7 @@ private static DbIntegrityCheckReadResult RunIntegrityCheckPragma(string dbPath)
if (IntegrityCheckRowsForTesting != null)
return BoundIntegrityRows(IntegrityCheckRowsForTesting());

var connectionString = dbPath.StartsWith("file:", StringComparison.OrdinalIgnoreCase)
? $"Data Source={dbPath}"
: new SqliteConnectionStringBuilder
{
DataSource = dbPath,
Mode = SqliteOpenMode.ReadOnly,
}.ConnectionString;
var connectionString = DbPathResolver.BuildSqliteConnectionString(dbPath, SqliteOpenMode.ReadOnly);

using var connection = new SqliteConnection(connectionString);
connection.Open();
Expand Down Expand Up @@ -571,13 +565,9 @@ FROM reference_lines rl

private static SqliteConnection OpenConnection(string dbPath, bool writable)
{
var connectionString = dbPath.StartsWith("file:", StringComparison.OrdinalIgnoreCase)
? $"Data Source={dbPath}"
: new SqliteConnectionStringBuilder
{
DataSource = dbPath,
Mode = writable ? SqliteOpenMode.ReadWrite : SqliteOpenMode.ReadOnly,
}.ConnectionString;
var connectionString = DbPathResolver.BuildSqliteConnectionString(
dbPath,
writable ? SqliteOpenMode.ReadWrite : SqliteOpenMode.ReadOnly);
var connection = new SqliteConnection(connectionString);
connection.Open();
return connection;
Expand Down
21 changes: 12 additions & 9 deletions src/CodeIndex/Cli/DbPathResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,17 @@ public static bool TryResolveWritableMutationDbPath(string dbPath, out string wr
return true;
}

internal static string BuildSqliteConnectionString(string dbPath, SqliteOpenMode? mode = null)
{
var builder = new SqliteConnectionStringBuilder
{
DataSource = dbPath,
};
if (mode.HasValue)
builder.Mode = mode.Value;
return builder.ConnectionString;
}

private static string? TryReadIndexedProjectRoot(string dbPath)
=> TryReadMetaString(dbPath, CodeIndex.Database.DbContext.IndexedProjectRootMetaKey);

Expand Down Expand Up @@ -412,15 +423,7 @@ private static bool SiblingRootMatchesIndexedContents(string dbPath, string full

private static SqliteConnection OpenMetadataConnection(string dbPath)
{
if (dbPath.StartsWith("file:", StringComparison.OrdinalIgnoreCase) && UriRequestsReadOnly(dbPath))
return new SqliteConnection($"Data Source={dbPath}");

var builder = new SqliteConnectionStringBuilder
{
DataSource = dbPath,
Mode = SqliteOpenMode.ReadOnly,
};
return new SqliteConnection(builder.ConnectionString);
return new SqliteConnection(BuildSqliteConnectionString(dbPath, SqliteOpenMode.ReadOnly));
}

public static bool UriRequestsReadOnly(string uriText)
Expand Down
8 changes: 1 addition & 7 deletions src/CodeIndex/Cli/DiffCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -626,13 +626,7 @@ private static SqliteConnection OpenReadOnlyConnection(string dbPath)
if (!isUri && !File.Exists(LongPath.EnsureWindowsPrefix(dbPath)))
throw new IOException($"database not found: {dbPath}");

var connectionString = isUri
? $"Data Source={dbPath}"
: new SqliteConnectionStringBuilder
{
DataSource = dbPath,
Mode = SqliteOpenMode.ReadOnly,
}.ConnectionString;
var connectionString = DbPathResolver.BuildSqliteConnectionString(dbPath, SqliteOpenMode.ReadOnly);

var connection = new SqliteConnection(connectionString);
connection.Open();
Expand Down
2 changes: 1 addition & 1 deletion src/CodeIndex/Database/DbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ public DbContext(string dbPath)
{
try
{
_connection = new SqliteConnection($"Data Source={dbPath}");
_connection = new SqliteConnection(DbPathResolver.BuildSqliteConnectionString(dbPath, SqliteOpenMode.ReadOnly));
_connection.Open();
Execute("PRAGMA busy_timeout=5000");
ApplyConnectionPerformancePragmas();
Expand Down
21 changes: 21 additions & 0 deletions tests/CodeIndex.Tests/DbCommandRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,27 @@ public void Run_MissingDb_ReturnsNotFoundWithHint()
Assert.Contains("cdidx index <projectPath>", stderr);
}

[Fact]
public void Run_IntegrityCheck_FileUriSemicolonPayloadDoesNotCreateDatabase_Issue3220()
{
var missingDb = Path.Combine(Path.GetTempPath(), $"cdidx_db_uri_injection_{Guid.NewGuid():N}.db");
var uri = new Uri(missingDb).AbsoluteUri + ";Mode=ReadWriteCreate";
try
{
var (exitCode, _, stderr) = RunAndCaptureStreams(["--integrity-check", "--db", uri]);

Assert.Equal(CommandExitCodes.DatabaseError, exitCode);
Assert.Contains("failed to run integrity check", stderr);
Assert.False(File.Exists(missingDb));
}
finally
{
SqliteConnection.ClearAllPools();
if (File.Exists(missingDb))
File.Delete(missingDb);
}
}

[Fact]
public void Run_MissingDb_JsonShapeIncludesHint()
{
Expand Down
13 changes: 13 additions & 0 deletions tests/CodeIndex.Tests/DbPathResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ public void ResolveForIndex_PrefersExplicitPath()
Assert.Equal(explicitPath, dbPath);
}

[Fact]
public void BuildSqliteConnectionString_FileUriKeepsSemicolonPayloadInDataSource_Issue3220()
{
const string uri = "file:///tmp/codeindex.db?immutable=1;Mode=ReadWriteCreate;Cache=Shared";

var connectionString = DbPathResolver.BuildSqliteConnectionString(uri, SqliteOpenMode.ReadOnly);
var parsed = new SqliteConnectionStringBuilder(connectionString);

Assert.Equal(uri, parsed.DataSource);
Assert.Equal(SqliteOpenMode.ReadOnly, parsed.Mode);
Assert.NotEqual(SqliteOpenMode.ReadWriteCreate, parsed.Mode);
}

[Fact]
public void ResolveForIndex_PrefersExplicitDataDirWhenDbPathMissing()
{
Expand Down
Loading