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/3379.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3379
affected:
- src/CodeIndex/Cli/DbCommandRunner.cs
- tests/CodeIndex.Tests/DbCommandRunnerTests.cs
---

## English

- **Temporary checkpoint cleanup now rejects unsafe recursive-delete targets (#3379)** — checkpoint and restore cleanup paths now require temporary directories to live under an explicit safe root and match the expected temporary-name prefix before recursive deletion is attempted.

## 日本語

- **checkpoint の一時クリーンアップが安全でない recursive delete 対象を拒否するようになりました (#3379)** — checkpoint / restore の cleanup は、recursive delete の前に一時ディレクトリが明示された safe root 配下にあり、期待される一時名 prefix と一致することを確認します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3426.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3426
affected:
- src/CodeIndex/Lsp/LspServer.cs
- tests/CodeIndex.Tests/LspServerTests.cs
---

## English

- **LSP path resolution now applies explicit rootless trust rules (#3426)** — workspace containment now uses the shared path-casing helper, rootless LSP requests only trust relative indexed paths after a workspace folder is known, and position reads remain bounded if files grow while being read.

## 日本語

- **LSP のパス解決が明示的な rootless trust rules を適用するようになりました (#3426)** — workspace containment は共有の path-casing helper を使い、rootless LSP request は workspace folder が判明した後だけ相対 indexed path を信頼し、position read は読み取り中にファイルが増えても上限内に収めます。
17 changes: 17 additions & 0 deletions changelog.d/unreleased/3430.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: security
issues:
- 3430
affected:
- src/CodeIndex/Cli/ActiveWorkspace.cs
- src/CodeIndex/Cli/WorkspaceCommandRunner.cs
- tests/CodeIndex.Tests/WorkspaceCommandRunnerTests.cs
---

## English

- **Active workspace state now validates root, db_path, and config-home inputs (#3430)** — malformed active workspace state must provide absolute root and database paths with the database under the active root, invalid config-home values are rejected before path composition, and warnings avoid echoing untrusted path values.

## 日本語

- **active workspace state が root、db_path、config-home 入力を検証するようになりました (#3430)** — active workspace state は絶対 root / database path と active root 配下の database を必須にし、不正な config-home 値は path 合成前に拒否し、警告では未信頼の path 値を出力しません。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3431.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3431
affected:
- src/CodeIndex/Cli/CdidxConfigFile.cs
- tests/CodeIndex.Tests/CdidxConfigFileTests.cs
---

## English

- **cdidx config validation now bounds scalar, path, and numeric values (#3431)** — config loading enforces field-specific string limits, validates MCP rate-limit numbers before staging them into environment settings, and reports invalid path values with sanitized stable diagnostics.

## 日本語

- **cdidx config validation が scalar、path、numeric value を上限検証するようになりました (#3431)** — config load は field-specific string limit を適用し、MCP rate-limit 数値を環境設定へ展開する前に検証し、不正な path 値は sanitize された安定診断で報告します。
17 changes: 17 additions & 0 deletions changelog.d/unreleased/3514.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: security
issues:
- 3514
affected:
- src/CodeIndex/Cli/DbCommandRunner.cs
- src/CodeIndex/Cli/JsonOutputContracts.cs
- tests/CodeIndex.Tests/DbCommandRunnerTests.cs
---

## English

- **Database checkpoint operations now report hardened cleanup and restore diagnostics (#3514)** — checkpoint listing surfaces sanitized diagnostics, restore preserves the primary failure when rollback also fails, checkpoint restore rejects symlinked or non-regular payload files, and prune reports WAL cleanup warnings after a committed apply.

## 日本語

- **database checkpoint 操作が hardened cleanup / restore 診断を返すようになりました (#3514)** — checkpoint list は sanitized diagnostics を返し、restore は rollback 失敗時も元の失敗を保持し、symlink や通常ファイルではない checkpoint payload を拒否し、prune apply 後の WAL cleanup 警告を報告します。
168 changes: 156 additions & 12 deletions src/CodeIndex/Cli/ActiveWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@ internal static string StatePath
{
get
{
var configHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
var root = string.IsNullOrWhiteSpace(configHome)
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config")
: configHome;
return Path.Combine(root, "cdidx", "active.json");
if (TryGetStatePath(out var path, out var reason))
return path;
throw new InvalidOperationException($"Active workspace state path is invalid: {reason}.");
}
}

Expand All @@ -34,7 +32,12 @@ internal static string StatePath
if (!string.IsNullOrWhiteSpace(envPath))
return LoadFromEnvironment(envPath);

var path = StatePath;
if (!TryGetStatePath(out var path, out var statePathReason))
{
WriteLoadWarning("config home", statePathReason);
return null;
}

if (!File.Exists(LongPath.EnsureWindowsPrefix(path)))
return null;

Expand All @@ -50,11 +53,15 @@ internal static string StatePath
using var document = JsonDocument.Parse(text, StateJsonDocumentOptions);
var root = document.RootElement;
var name = ReadString(root, "name") ?? "default";
var workspaceRoot = ReadString(root, "root") ?? Environment.CurrentDirectory;
var workspaceRoot = ReadString(root, "root");
var dbPath = ReadString(root, "db_path");
if (string.IsNullOrWhiteSpace(dbPath))
if (!TryNormalizeState(name, workspaceRoot, dbPath, out var state, out var stateReason))
{
WriteLoadWarning("state file", stateReason);
return null;
return new ActiveWorkspaceState(name, Path.GetFullPath(workspaceRoot), Path.GetFullPath(dbPath));
}

return state;
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException)
{
Expand All @@ -65,9 +72,13 @@ internal static string StatePath

internal static void Save(ActiveWorkspaceState state)
{
DataDirectorySecurity.CreateSensitiveDirectory(Path.GetDirectoryName(StatePath)!);
var payload = new ActiveWorkspaceState(state.Name, Path.GetFullPath(state.Root), Path.GetFullPath(state.DbPath));
DataDirectorySecurity.WritePrivateText(StatePath, JsonSerializer.Serialize(payload, ProgramRunner.CreateDefaultJsonOptions()));
if (!TryGetStatePath(out var statePath, out var statePathReason))
throw new InvalidOperationException($"Active workspace state path is invalid: {statePathReason}.");
if (!TryNormalizeState(state.Name, state.Root, state.DbPath, out var payload, out var stateReason))
throw new InvalidOperationException($"Active workspace state is invalid: {stateReason}.");

DataDirectorySecurity.CreateSensitiveDirectory(Path.GetDirectoryName(statePath)!);
DataDirectorySecurity.WritePrivateText(statePath, JsonSerializer.Serialize(payload, ProgramRunner.CreateDefaultJsonOptions()));
}

private static string? ReadString(JsonElement element, string name)
Expand All @@ -93,6 +104,139 @@ internal static void Save(ActiveWorkspaceState state)
}
}

private static bool TryGetStatePath(out string path, out string reason)
{
path = string.Empty;
reason = string.Empty;
var configHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
string root;
if (string.IsNullOrWhiteSpace(configHome))
{
var profile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (string.IsNullOrWhiteSpace(profile))
{
reason = "user profile directory is unavailable";
return false;
}

root = Path.Combine(profile, ".config");
}
else
{
if (configHome.Length > MaxEnvironmentPathChars)
{
reason = $"XDG_CONFIG_HOME exceeds {MaxEnvironmentPathChars} characters";
return false;
}

if (!IsFullyQualifiedPath(configHome))
{
reason = "XDG_CONFIG_HOME must be an absolute path";
return false;
}

root = configHome;
}

try
{
path = Path.Combine(NormalizeBoundaryPath(root), "cdidx", "active.json");
return true;
}
catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or PathTooLongException)
{
reason = "XDG_CONFIG_HOME is invalid";
return false;
}
}

private static bool TryNormalizeState(
string? name,
string? root,
string? dbPath,
out ActiveWorkspaceState? state,
out string reason)
{
state = null;
reason = string.Empty;
if (string.IsNullOrWhiteSpace(root))
{
reason = "`root` is required";
return false;
}

if (string.IsNullOrWhiteSpace(dbPath))
{
reason = "`db_path` is required";
return false;
}

if (root.Length > MaxEnvironmentPathChars)
{
reason = $"`root` exceeds {MaxEnvironmentPathChars} characters";
return false;
}

if (dbPath.Length > MaxEnvironmentPathChars)
{
reason = $"`db_path` exceeds {MaxEnvironmentPathChars} characters";
return false;
}

if (!IsFullyQualifiedPath(root))
{
reason = "`root` must be an absolute path";
return false;
}

if (!IsFullyQualifiedPath(dbPath))
{
reason = "`db_path` must be an absolute path";
return false;
}

try
{
var normalizedRoot = NormalizeBoundaryPath(root);
var normalizedDbPath = Path.GetFullPath(dbPath);
if (PathCasing.PathsEqual(normalizedRoot, normalizedDbPath)
|| !PathCasing.IsPathEqualOrParent(normalizedRoot, normalizedDbPath))
{
reason = "`db_path` must be inside `root`";
return false;
}

state = new ActiveWorkspaceState(string.IsNullOrWhiteSpace(name) ? "default" : name, normalizedRoot, normalizedDbPath);
return true;
}
catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or PathTooLongException)
{
reason = "state paths are invalid";
return false;
}
}

private static string NormalizeBoundaryPath(string path)
{
var fullPath = Path.GetFullPath(path);
var root = Path.GetPathRoot(fullPath);
if (!string.IsNullOrEmpty(root) && string.Equals(fullPath, root, StringComparison.Ordinal))
return fullPath;
return fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}

private static bool IsFullyQualifiedPath(string path)
{
try
{
return Path.IsPathFullyQualified(path);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
{
return false;
}
}

private static string DescribeLoadFailure(Exception ex) => ex switch
{
JsonException => "invalid JSON",
Expand Down
Loading
Loading