diff --git a/changelog.d/unreleased/3025.fixed.md b/changelog.d/unreleased/3025.fixed.md new file mode 100644 index 0000000000..09c01df2d1 --- /dev/null +++ b/changelog.d/unreleased/3025.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3025 +affected: + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs +--- + +## English + +- **Version metadata loading now falls back on invalid files (#3025)** — malformed, missing, unreadable, or schema-invalid `version.json` files no longer throw from CLI version-display paths and instead return the existing `0.0.0` fallback. + +## 日本語 + +- **不正な version metadata 読み込み時に fallback するようになりました (#3025)** — malformed、missing、unreadable、schema-invalid な `version.json` でも CLI の version 表示経路で例外を投げず、既存の `0.0.0` fallback を返します。 diff --git a/changelog.d/unreleased/3244.fixed.md b/changelog.d/unreleased/3244.fixed.md new file mode 100644 index 0000000000..8b902792c6 --- /dev/null +++ b/changelog.d/unreleased/3244.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3244 +affected: + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs +--- + +## English + +- **Version metadata reads are now bounded (#3244)** — `version.json` loading now reads at most 16 KiB and parses with an explicit JSON depth limit before falling back to `0.0.0` on invalid metadata. + +## 日本語 + +- **version metadata の読み込みに上限を設定しました (#3244)** — `version.json` の読み込みは最大 16 KiB になり、明示的な JSON depth 上限で parse したうえで、不正な metadata では `0.0.0` に fallback します。 diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 9ea82aebce..fe66e6528e 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Text; +using System.Text.Json; namespace CodeIndex.Cli; @@ -67,6 +68,14 @@ public static class ConsoleUi public const string DisableProgressEnvironmentVariable = "CDIDX_DISABLE_PROGRESS"; public const string PrefersReducedMotionEnvironmentVariable = "PREFERS_REDUCED_MOTION"; public const int SummaryLabelWidth = 9; + internal const int MaxVersionJsonBytes = 16 * 1024; + internal const int MaxVersionJsonDepth = 8; + private const string FallbackVersion = "0.0.0"; + + private static readonly JsonDocumentOptions VersionJsonDocumentOptions = new() + { + MaxDepth = MaxVersionJsonDepth, + }; private static readonly (string Command, string Usage)[] CommandUsageLines = [ @@ -714,13 +723,32 @@ public static string LoadVersion() } var ioPath = LongPath.EnsureWindowsPrefix(path); if (File.Exists(ioPath)) + return LoadVersionFromFile(ioPath); + + return FallbackVersion; + } + + internal static string LoadVersionFromFile(string ioPath) + { + try { - var json = File.ReadAllText(ioPath); - using var doc = System.Text.Json.JsonDocument.Parse(json); + var json = DataDirectorySecurity.ReadTextWithinLimit(ioPath, MaxVersionJsonBytes); + if (json is null) + return FallbackVersion; + + using var doc = JsonDocument.Parse(json, VersionJsonDocumentOptions); if (doc.RootElement.TryGetProperty("version", out var ver)) - return ver.GetString() ?? "0.0.0"; + return ver.GetString() ?? FallbackVersion; } - return "0.0.0"; + catch (Exception ex) when (ex is IOException + or UnauthorizedAccessException + or JsonException + or InvalidOperationException) + { + return FallbackVersion; + } + + return FallbackVersion; } /// diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 3615332ed7..7d9f7f1bd6 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -640,6 +640,83 @@ public void LoadVersion_ReturnsActualVersion_NotFallback() Assert.Contains('.', version); } + [Fact] + public void LoadVersionFromFile_MalformedJson_ReturnsFallback() + { + var path = WriteTempVersionJson("""{"version":"""); + try + { + Assert.Equal("0.0.0", ConsoleUi.LoadVersionFromFile(path)); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void LoadVersionFromFile_NonStringVersion_ReturnsFallback() + { + var path = WriteTempVersionJson("""{"version":{}}"""); + try + { + Assert.Equal("0.0.0", ConsoleUi.LoadVersionFromFile(path)); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void LoadVersionFromFile_MissingFile_ReturnsFallback() + { + var path = Path.Combine(Path.GetTempPath(), $"cdidx_missing_version_{Guid.NewGuid():N}.json"); + + Assert.Equal("0.0.0", ConsoleUi.LoadVersionFromFile(path)); + } + + [Fact] + public void LoadVersionFromFile_OversizedJson_ReturnsFallback() + { + var path = WriteTempVersionJson("{\"version\":\"" + new string('1', ConsoleUi.MaxVersionJsonBytes) + "\"}"); + try + { + Assert.Equal("0.0.0", ConsoleUi.LoadVersionFromFile(path)); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void LoadVersionFromFile_TooDeepJson_ReturnsFallback() + { + var nesting = ConsoleUi.MaxVersionJsonDepth + 1; + var path = WriteTempVersionJson( + """{"version":"1.2.3","nested":""" + + new string('[', nesting) + + "0" + + new string(']', nesting) + + "}"); + try + { + Assert.Equal("0.0.0", ConsoleUi.LoadVersionFromFile(path)); + } + finally + { + File.Delete(path); + } + } + + private static string WriteTempVersionJson(string content) + { + var path = Path.Combine(Path.GetTempPath(), $"cdidx_version_{Guid.NewGuid():N}.json"); + File.WriteAllText(path, content, Encoding.UTF8); + return path; + } + [Fact] public void LoadBuildMetadata_PopulatesAllFields() {