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 "..")