mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
feat(config): 添加配置表来源安全性验证功能
- 在 GodotYamlConfigLoader 中增加对路径中冒号字符的验证,防止 Windows 无效名称和 ADS 类似语法 - 新增 GodotYamlConfigTableSource 类用于描述配置表来源信息,并实现安全路径验证 - 添加对配置路径和 schema 路径的严格安全检查,拒绝包含根路径、遍历标记或冒号字符的路径 - 扩展测试用例覆盖多种不安全路径场景,包括路径遍历、绝对路径前缀和冒号字符 - 为新功能添加完整的单元测试验证安全路径验证逻辑
This commit is contained in:
parent
82091be03c
commit
c29c9fe8f4
@ -6,9 +6,7 @@ using System.Reflection;
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GFramework.Game.Abstractions.Config;
|
using GFramework.Game.Abstractions.Config;
|
||||||
using GFramework.Game.Config;
|
|
||||||
using GFramework.Godot.Config;
|
using GFramework.Godot.Config;
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.Tests.Config;
|
namespace GFramework.Godot.Tests.Config;
|
||||||
|
|
||||||
@ -18,6 +16,10 @@ namespace GFramework.Godot.Tests.Config;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public sealed class GodotYamlConfigLoaderTests
|
public sealed class GodotYamlConfigLoaderTests
|
||||||
{
|
{
|
||||||
|
private string _resourceRoot = null!;
|
||||||
|
private string _testRoot = null!;
|
||||||
|
private string _userRoot = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为每个测试准备独立的资源根目录与用户目录。
|
/// 为每个测试准备独立的资源根目录与用户目录。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -46,10 +48,6 @@ public sealed class GodotYamlConfigLoaderTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _resourceRoot = null!;
|
|
||||||
private string _testRoot = null!;
|
|
||||||
private string _userRoot = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证导出态会把注册过的 YAML 与 schema 文本同步到运行时缓存,再交给底层加载器。
|
/// 验证导出态会把注册过的 YAML 与 schema 文本同步到运行时缓存,再交给底层加载器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -205,10 +203,12 @@ public sealed class GodotYamlConfigLoaderTests
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证加载器自身会拒绝可能逃逸缓存根目录的非法配置目录路径,即使调用方绕过了公开构造约束。
|
/// 验证加载器自身会拒绝可能逃逸缓存根目录的非法配置目录路径,即使调用方绕过了公开构造约束。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[TestCase("../outside")]
|
||||||
public void LoadAsync_Should_Reject_Invalid_Config_Relative_Path_When_Metadata_Is_Corrupted()
|
[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(
|
var loader = CreateLoader(
|
||||||
isEditor: false,
|
isEditor: false,
|
||||||
tableSources: [corruptedSource],
|
tableSources: [corruptedSource],
|
||||||
@ -223,8 +223,10 @@ public sealed class GodotYamlConfigLoaderTests
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证加载器自身会拒绝可能逃逸缓存根目录的非法 schema 路径,即使调用方绕过了公开构造约束。
|
/// 验证加载器自身会拒绝可能逃逸缓存根目录的非法 schema 路径,即使调用方绕过了公开构造约束。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[TestCase("../schemas/monster.schema.json")]
|
||||||
public void LoadAsync_Should_Reject_Invalid_Schema_Relative_Path_When_Metadata_Is_Corrupted()
|
[TestCase("schemas:bad/monster.schema.json")]
|
||||||
|
public void LoadAsync_Should_Reject_Invalid_Schema_Relative_Path_When_Metadata_Is_Corrupted(
|
||||||
|
string schemaRelativePath)
|
||||||
{
|
{
|
||||||
WriteFile(
|
WriteFile(
|
||||||
_resourceRoot,
|
_resourceRoot,
|
||||||
@ -235,7 +237,7 @@ public sealed class GodotYamlConfigLoaderTests
|
|||||||
hp: 10
|
hp: 10
|
||||||
""");
|
""");
|
||||||
|
|
||||||
var corruptedSource = CreateUnsafeTableSource("monster", "monster", "../schemas/monster.schema.json");
|
var corruptedSource = CreateUnsafeTableSource("monster", "monster", schemaRelativePath);
|
||||||
var loader = CreateLoader(
|
var loader = CreateLoader(
|
||||||
isEditor: false,
|
isEditor: false,
|
||||||
tableSources: [corruptedSource],
|
tableSources: [corruptedSource],
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using GFramework.Godot.Config;
|
using GFramework.Godot.Config;
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.Tests.Config;
|
namespace GFramework.Godot.Tests.Config;
|
||||||
|
|
||||||
@ -27,6 +26,8 @@ public sealed class GodotYamlConfigTableSourceTests
|
|||||||
[TestCase(@"C:\monster")]
|
[TestCase(@"C:\monster")]
|
||||||
[TestCase("res://monster")]
|
[TestCase("res://monster")]
|
||||||
[TestCase("user://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)
|
public void Constructor_Should_Throw_When_Config_Relative_Path_Is_Not_Safe(string configRelativePath)
|
||||||
{
|
{
|
||||||
var exception = Assert.Throws<ArgumentException>(() =>
|
var exception = Assert.Throws<ArgumentException>(() =>
|
||||||
@ -52,6 +53,8 @@ public sealed class GodotYamlConfigTableSourceTests
|
|||||||
[TestCase(@"C:\schemas\monster.schema.json")]
|
[TestCase(@"C:\schemas\monster.schema.json")]
|
||||||
[TestCase("res://schemas/monster.schema.json")]
|
[TestCase("res://schemas/monster.schema.json")]
|
||||||
[TestCase("user://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)
|
public void Constructor_Should_Throw_When_Schema_Relative_Path_Is_Not_Safe(string schemaRelativePath)
|
||||||
{
|
{
|
||||||
var exception = Assert.Throws<ArgumentException>(() =>
|
var exception = Assert.Throws<ArgumentException>(() =>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ using GFramework.Core.Abstractions.Events;
|
|||||||
using GFramework.Game.Abstractions.Config;
|
using GFramework.Game.Abstractions.Config;
|
||||||
using GFramework.Game.Config;
|
using GFramework.Game.Config;
|
||||||
using GFramework.Godot.Extensions;
|
using GFramework.Godot.Extensions;
|
||||||
using FileAccess = Godot.FileAccess;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.Config;
|
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));
|
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);
|
var segments = normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (segments.Any(static segment => segment is "." or ".."))
|
if (segments.Any(static segment => segment is "." or ".."))
|
||||||
{
|
{
|
||||||
|
|||||||
@ -13,11 +13,12 @@ public sealed class GodotYamlConfigTableSource
|
|||||||
/// <param name="tableName">运行时表名称。</param>
|
/// <param name="tableName">运行时表名称。</param>
|
||||||
/// <param name="configRelativePath">
|
/// <param name="configRelativePath">
|
||||||
/// 相对配置根目录的 YAML 目录。
|
/// 相对配置根目录的 YAML 目录。
|
||||||
/// 该路径必须保持为无根相对路径,且不能包含 <c>.</c>、<c>..</c>、<c>res://</c>、<c>user://</c> 或磁盘根路径前缀。
|
/// 该路径必须保持为无根相对路径,且不能包含 <c>.</c>、<c>..</c>、<c>res://</c>、<c>user://</c>、<c>:</c>
|
||||||
|
/// 或磁盘根路径前缀。
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="schemaRelativePath">
|
/// <param name="schemaRelativePath">
|
||||||
/// 相对配置根目录的 schema 文件路径;未启用 schema 时为空。
|
/// 相对配置根目录的 schema 文件路径;未启用 schema 时为空。
|
||||||
/// 如果提供,同样必须保持为无根相对路径,且不能包含 <c>.</c>、<c>..</c> 或任何绝对路径前缀。
|
/// 如果提供,同样必须保持为无根相对路径,且不能包含 <c>.</c>、<c>..</c>、<c>:</c> 或任何绝对路径前缀。
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <exception cref="ArgumentException">
|
/// <exception cref="ArgumentException">
|
||||||
/// <paramref name="tableName" />、<paramref name="configRelativePath" /> 或 <paramref name="schemaRelativePath" />
|
/// <paramref name="tableName" />、<paramref name="configRelativePath" /> 或 <paramref name="schemaRelativePath" />
|
||||||
@ -72,13 +73,13 @@ public sealed class GodotYamlConfigTableSource
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取相对配置根目录的 YAML 目录路径。
|
/// 获取相对配置根目录的 YAML 目录路径。
|
||||||
/// 该值始终保持为无根相对路径,不会包含 <c>.</c> 或 <c>..</c> 段。
|
/// 该值始终保持为无根相对路径,不会包含 <c>.</c>、<c>..</c> 或 <c>:</c> 段。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ConfigRelativePath { get; }
|
public string ConfigRelativePath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取相对配置根目录的 schema 文件路径;未启用 schema 校验时为空。
|
/// 获取相对配置根目录的 schema 文件路径;未启用 schema 校验时为空。
|
||||||
/// 该值在非空时始终保持为无根相对路径,不会包含 <c>.</c> 或 <c>..</c> 段。
|
/// 该值在非空时始终保持为无根相对路径,不会包含 <c>.</c>、<c>..</c> 或 <c>:</c> 段。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? SchemaRelativePath { get; }
|
public string? SchemaRelativePath { get; }
|
||||||
|
|
||||||
@ -94,6 +95,11 @@ public sealed class GodotYamlConfigTableSource
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (normalizedPath.Contains(':', StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var segment in normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries))
|
foreach (var segment in normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries))
|
||||||
{
|
{
|
||||||
if (segment is "." or "..")
|
if (segment is "." or "..")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user