From c29c9fe8f46932d7f865304b9093411ced581890 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 11 Apr 2026 07:37:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E8=A1=A8=E6=9D=A5=E6=BA=90=E5=AE=89=E5=85=A8=E6=80=A7?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 GodotYamlConfigLoader 中增加对路径中冒号字符的验证,防止 Windows 无效名称和 ADS 类似语法 - 新增 GodotYamlConfigTableSource 类用于描述配置表来源信息,并实现安全路径验证 - 添加对配置路径和 schema 路径的严格安全检查,拒绝包含根路径、遍历标记或冒号字符的路径 - 扩展测试用例覆盖多种不安全路径场景,包括路径遍历、绝对路径前缀和冒号字符 - 为新功能添加完整的单元测试验证安全路径验证逻辑 --- .../Config/GodotYamlConfigLoaderTests.cs | 26 ++++++++++--------- .../Config/GodotYamlConfigTableSourceTests.cs | 5 +++- .../Config/GodotYamlConfigLoader.cs | 9 ++++++- .../Config/GodotYamlConfigTableSource.cs | 14 +++++++--- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs b/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs index 6aa7a8e2..bef0737a 100644 --- a/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs +++ b/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs @@ -6,9 +6,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; using GFramework.Game.Abstractions.Config; -using GFramework.Game.Config; using GFramework.Godot.Config; -using NUnit.Framework; namespace GFramework.Godot.Tests.Config; @@ -18,6 +16,10 @@ namespace GFramework.Godot.Tests.Config; [TestFixture] public sealed class GodotYamlConfigLoaderTests { + private string _resourceRoot = null!; + private string _testRoot = null!; + private string _userRoot = null!; + /// /// 为每个测试准备独立的资源根目录与用户目录。 /// @@ -46,10 +48,6 @@ public sealed class GodotYamlConfigLoaderTests } } - private string _resourceRoot = null!; - private string _testRoot = null!; - private string _userRoot = null!; - /// /// 验证导出态会把注册过的 YAML 与 schema 文本同步到运行时缓存,再交给底层加载器。 /// @@ -205,10 +203,12 @@ public sealed class GodotYamlConfigLoaderTests /// /// 验证加载器自身会拒绝可能逃逸缓存根目录的非法配置目录路径,即使调用方绕过了公开构造约束。 /// - [Test] - public void LoadAsync_Should_Reject_Invalid_Config_Relative_Path_When_Metadata_Is_Corrupted() + [TestCase("../outside")] + [TestCase("schemas:bad/monster")] + public void LoadAsync_Should_Reject_Invalid_Config_Relative_Path_When_Metadata_Is_Corrupted( + string configRelativePath) { - var corruptedSource = CreateUnsafeTableSource("monster", "../outside"); + var corruptedSource = CreateUnsafeTableSource("monster", configRelativePath); var loader = CreateLoader( isEditor: false, tableSources: [corruptedSource], @@ -223,8 +223,10 @@ public sealed class GodotYamlConfigLoaderTests /// /// 验证加载器自身会拒绝可能逃逸缓存根目录的非法 schema 路径,即使调用方绕过了公开构造约束。 /// - [Test] - public void LoadAsync_Should_Reject_Invalid_Schema_Relative_Path_When_Metadata_Is_Corrupted() + [TestCase("../schemas/monster.schema.json")] + [TestCase("schemas:bad/monster.schema.json")] + public void LoadAsync_Should_Reject_Invalid_Schema_Relative_Path_When_Metadata_Is_Corrupted( + string schemaRelativePath) { WriteFile( _resourceRoot, @@ -235,7 +237,7 @@ public sealed class GodotYamlConfigLoaderTests hp: 10 """); - var corruptedSource = CreateUnsafeTableSource("monster", "monster", "../schemas/monster.schema.json"); + var corruptedSource = CreateUnsafeTableSource("monster", "monster", schemaRelativePath); var loader = CreateLoader( isEditor: false, tableSources: [corruptedSource], diff --git a/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs b/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs index 5e4998ce..ccb8ab6a 100644 --- a/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs +++ b/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs @@ -1,6 +1,5 @@ using System; using GFramework.Godot.Config; -using NUnit.Framework; namespace GFramework.Godot.Tests.Config; @@ -27,6 +26,8 @@ public sealed class GodotYamlConfigTableSourceTests [TestCase(@"C:\monster")] [TestCase("res://monster")] [TestCase("user://monster")] + [TestCase("schemas:bad/monster")] + [TestCase(@"schemas:bad\monster")] public void Constructor_Should_Throw_When_Config_Relative_Path_Is_Not_Safe(string configRelativePath) { var exception = Assert.Throws(() => @@ -52,6 +53,8 @@ public sealed class GodotYamlConfigTableSourceTests [TestCase(@"C:\schemas\monster.schema.json")] [TestCase("res://schemas/monster.schema.json")] [TestCase("user://schemas/monster.schema.json")] + [TestCase("schemas:bad/monster.schema.json")] + [TestCase(@"schemas:bad\monster.schema.json")] public void Constructor_Should_Throw_When_Schema_Relative_Path_Is_Not_Safe(string schemaRelativePath) { var exception = Assert.Throws(() => diff --git a/GFramework.Godot/Config/GodotYamlConfigLoader.cs b/GFramework.Godot/Config/GodotYamlConfigLoader.cs index e4cf2c09..0e32867a 100644 --- a/GFramework.Godot/Config/GodotYamlConfigLoader.cs +++ b/GFramework.Godot/Config/GodotYamlConfigLoader.cs @@ -3,7 +3,6 @@ using GFramework.Core.Abstractions.Events; using GFramework.Game.Abstractions.Config; using GFramework.Game.Config; using GFramework.Godot.Extensions; -using FileAccess = Godot.FileAccess; namespace GFramework.Godot.Config; @@ -440,6 +439,14 @@ public sealed class GodotYamlConfigLoader : IConfigLoader throw new ArgumentException("Relative path must be an unrooted path.", nameof(relativePath)); } + // Reject ':' in later segments as well so Windows-invalid names and ADS-like syntax never reach file APIs. + if (normalizedPath.Contains(':', StringComparison.Ordinal)) + { + throw new ArgumentException( + "Relative path must not contain ':' characters.", + nameof(relativePath)); + } + var segments = normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries); if (segments.Any(static segment => segment is "." or "..")) { diff --git a/GFramework.Godot/Config/GodotYamlConfigTableSource.cs b/GFramework.Godot/Config/GodotYamlConfigTableSource.cs index 4d28826a..c851751d 100644 --- a/GFramework.Godot/Config/GodotYamlConfigTableSource.cs +++ b/GFramework.Godot/Config/GodotYamlConfigTableSource.cs @@ -13,11 +13,12 @@ public sealed class GodotYamlConfigTableSource /// 运行时表名称。 /// /// 相对配置根目录的 YAML 目录。 - /// 该路径必须保持为无根相对路径,且不能包含 ...res://user:// 或磁盘根路径前缀。 + /// 该路径必须保持为无根相对路径,且不能包含 ...res://user://: + /// 或磁盘根路径前缀。 /// /// /// 相对配置根目录的 schema 文件路径;未启用 schema 时为空。 - /// 如果提供,同样必须保持为无根相对路径,且不能包含 ... 或任何绝对路径前缀。 + /// 如果提供,同样必须保持为无根相对路径,且不能包含 ...: 或任何绝对路径前缀。 /// /// /// @@ -72,13 +73,13 @@ public sealed class GodotYamlConfigTableSource /// /// 获取相对配置根目录的 YAML 目录路径。 - /// 该值始终保持为无根相对路径,不会包含 ... 段。 + /// 该值始终保持为无根相对路径,不会包含 ...: 段。 /// public string ConfigRelativePath { get; } /// /// 获取相对配置根目录的 schema 文件路径;未启用 schema 校验时为空。 - /// 该值在非空时始终保持为无根相对路径,不会包含 ... 段。 + /// 该值在非空时始终保持为无根相对路径,不会包含 ...: 段。 /// public string? SchemaRelativePath { get; } @@ -94,6 +95,11 @@ public sealed class GodotYamlConfigTableSource return false; } + if (normalizedPath.Contains(':', StringComparison.Ordinal)) + { + return false; + } + foreach (var segment in normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries)) { if (segment is "." or "..")