Skip to content

Commit e00f9f5

Browse files
committed
fix: 鉴权强制 + HTTPS + 路径遍历防护
- 新增 AuthenticationMiddleware:5001 端口未认证请求 401\n- 新增 PathGuard:路径基目录约束工具\n- ServerManager:HTTPS + #if !DEBUG + 注册中间件\n- GetLocalImagePreview/DdsExtractor/Emote:路径限制到游戏目录内\n- EmoteController:ArgumentList 防注入 + ExitCode 检查
1 parent d6a5f74 commit e00f9f5

6 files changed

Lines changed: 135 additions & 36 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace ChuChartManager;
2+
3+
public class AuthenticationMiddleware
4+
{
5+
private readonly RequestDelegate _next;
6+
7+
public AuthenticationMiddleware(RequestDelegate next)
8+
{
9+
_next = next;
10+
}
11+
12+
public async Task Invoke(HttpContext context)
13+
{
14+
if (!context.User.Identity?.IsAuthenticated == true && context.Connection.LocalPort == 5001)
15+
{
16+
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
17+
context.Response.Headers.Append("WWW-Authenticate", "Basic");
18+
await context.Response.WriteAsync("Unauthorized");
19+
return;
20+
}
21+
22+
await _next(context);
23+
}
24+
}

ChuChartManager/Controllers/CustomResourceController.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,12 @@ public ActionResult GetLocalImagePreview([FromQuery] string path)
413413
if (string.IsNullOrWhiteSpace(path) || !System.IO.File.Exists(path))
414414
return NotFound();
415415

416+
var safe = PathGuard.EnsureWithin(StaticSettings.GamePath, path)
417+
?? PathGuard.EnsureWithin(StaticSettings.AppDataDir, path)
418+
?? PathGuard.EnsureWithin(StaticSettings.ExeDir, path);
419+
if (safe == null)
420+
return Forbid();
421+
416422
var ext = Path.GetExtension(path).ToLowerInvariant();
417423
var mime = ext switch
418424
{

ChuChartManager/Controllers/DdsExtractorController.cs

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class ExtractResult
1212
public int DdsCount { get; set; }
1313
public string OutputDir { get; set; } = "";
1414
public List<string> Files { get; set; } = [];
15+
public string? Error { get; set; }
1516
}
1617

1718
public class ExtractDdsRequest
@@ -69,22 +70,35 @@ public ActionResult<List<ExtractResult>> ExtractDds([FromBody] ExtractDdsRequest
6970
if (string.IsNullOrWhiteSpace(request.Path))
7071
return BadRequest("路径不能为空");
7172

73+
if (!string.IsNullOrWhiteSpace(request.OutputDir))
74+
{
75+
var safeOut = PathGuard.EnsureWithin(StaticSettings.GamePath, request.OutputDir);
76+
if (safeOut == null)
77+
return BadRequest("输出目录不在游戏目录范围内");
78+
request.OutputDir = safeOut;
79+
}
80+
7281
var results = new List<ExtractResult>();
7382

7483
if (System.IO.File.Exists(request.Path))
7584
{
76-
var result = ExtractFromFile(request.Path, request.OutputDir);
77-
if (result != null) results.Add(result);
85+
var safeIn = PathGuard.EnsureWithin(StaticSettings.GamePath, request.Path);
86+
if (safeIn == null)
87+
return BadRequest("源文件不在游戏目录范围内");
88+
results.Add(ExtractFromFile(safeIn, request.OutputDir));
7889
}
7990
else if (Directory.Exists(request.Path))
8091
{
81-
var files = Directory.GetFiles(request.Path, "*.afb", SearchOption.TopDirectoryOnly)
82-
.Concat(Directory.GetFiles(request.Path, "*.svo", SearchOption.TopDirectoryOnly));
92+
var safeIn = PathGuard.EnsureWithin(StaticSettings.GamePath, request.Path);
93+
if (safeIn == null)
94+
return BadRequest("源目录不在游戏目录范围内");
95+
96+
var files = Directory.GetFiles(safeIn, "*.afb", SearchOption.TopDirectoryOnly)
97+
.Concat(Directory.GetFiles(safeIn, "*.svo", SearchOption.TopDirectoryOnly));
8398

8499
foreach (var file in files)
85100
{
86-
var result = ExtractFromFile(file, request.OutputDir);
87-
if (result != null) results.Add(result);
101+
results.Add(ExtractFromFile(file, request.OutputDir));
88102
}
89103
}
90104
else
@@ -95,36 +109,45 @@ public ActionResult<List<ExtractResult>> ExtractDds([FromBody] ExtractDdsRequest
95109
return Ok(results);
96110
}
97111

98-
private static ExtractResult? ExtractFromFile(string filePath, string? outputDirOverride)
112+
private static ExtractResult ExtractFromFile(string filePath, string? outputDirOverride)
99113
{
100-
var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant();
101-
if (ext != ".afb" && ext != ".svo") return null;
114+
try
115+
{
116+
var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant();
117+
if (ext != ".afb" && ext != ".svo")
118+
return new ExtractResult { SourceFile = filePath, Error = $"不支持的文件格式: {ext}" };
102119

103-
var fileData = System.IO.File.ReadAllBytes(filePath);
104-
var ddsList = DDSExtractor.DdsExtractor.ExtractDdsFiles(fileData, ext == ".afb");
105-
if (ddsList.Count == 0) return null;
120+
var fileData = System.IO.File.ReadAllBytes(filePath);
121+
var ddsList = DDSExtractor.DdsExtractor.ExtractDdsFiles(fileData, ext == ".afb");
122+
if (ddsList.Count == 0)
123+
return new ExtractResult { SourceFile = filePath, Error = "未提取到 DDS" };
106124

107-
var baseName = System.IO.Path.GetFileNameWithoutExtension(filePath);
108-
var outputDir = !string.IsNullOrWhiteSpace(outputDirOverride)
109-
? System.IO.Path.Combine(outputDirOverride, $"{baseName}_extracted")
110-
: System.IO.Path.Combine(System.IO.Path.GetDirectoryName(filePath)!, $"{baseName}_extracted");
125+
var baseName = System.IO.Path.GetFileNameWithoutExtension(filePath);
126+
var outputDir = !string.IsNullOrWhiteSpace(outputDirOverride)
127+
? System.IO.Path.Combine(outputDirOverride, $"{baseName}_extracted")
128+
: System.IO.Path.Combine(System.IO.Path.GetDirectoryName(filePath)!, $"{baseName}_extracted");
111129

112-
Directory.CreateDirectory(outputDir);
130+
Directory.CreateDirectory(outputDir);
113131

114-
var savedFiles = new List<string>();
115-
for (var i = 0; i < ddsList.Count; i++)
116-
{
117-
var outputPath = System.IO.Path.Combine(outputDir, $"{baseName}_{i + 1}.dds");
118-
System.IO.File.WriteAllBytes(outputPath, ddsList[i]);
119-
savedFiles.Add(outputPath);
120-
}
132+
var savedFiles = new List<string>();
133+
for (var i = 0; i < ddsList.Count; i++)
134+
{
135+
var outputPath = System.IO.Path.Combine(outputDir, $"{baseName}_{i + 1}.dds");
136+
System.IO.File.WriteAllBytes(outputPath, ddsList[i]);
137+
savedFiles.Add(outputPath);
138+
}
121139

122-
return new ExtractResult
140+
return new ExtractResult
141+
{
142+
SourceFile = filePath,
143+
DdsCount = ddsList.Count,
144+
OutputDir = outputDir,
145+
Files = savedFiles,
146+
};
147+
}
148+
catch (Exception ex)
123149
{
124-
SourceFile = filePath,
125-
DdsCount = ddsList.Count,
126-
OutputDir = outputDir,
127-
Files = savedFiles,
128-
};
150+
return new ExtractResult { SourceFile = filePath, Error = ex.Message };
151+
}
129152
}
130153
}

ChuChartManager/Controllers/EmoteController.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ public class EmoteDataItem
1717
public long FileSize { get; set; }
1818
}
1919

20+
private static string? SafeGameFilePath(string filePath)
21+
{
22+
if (string.IsNullOrEmpty(StaticSettings.GamePath)) return null;
23+
return PathGuard.FileExistsWithin(StaticSettings.GamePath, filePath, out var safe) ? safe : null;
24+
}
25+
2026
[HttpGet]
2127
public ActionResult<List<EmoteDataItem>> GetEmoteDataList([FromQuery] string? source = null)
2228
{
@@ -54,8 +60,8 @@ public ActionResult LaunchViewer([FromBody] LaunchViewerRequest request)
5460
if (string.IsNullOrWhiteSpace(request.FilePath))
5561
return BadRequest("文件路径不能为空");
5662

57-
if (!System.IO.File.Exists(request.FilePath))
58-
return BadRequest("文件不存在");
63+
if (SafeGameFilePath(request.FilePath) == null)
64+
return BadRequest("文件不在游戏目录范围内");
5965

6066
var viewerPath = Path.Combine(StaticSettings.ExeDir, "tools", "FreeMoteViewer.exe");
6167
if (!System.IO.File.Exists(viewerPath))
@@ -93,7 +99,7 @@ public class LaunchViewerRequest
9399
[HttpGet]
94100
public ActionResult GetEmoteWebGLData([FromQuery] string filePath)
95101
{
96-
if (string.IsNullOrWhiteSpace(filePath) || !System.IO.File.Exists(filePath))
102+
if (SafeGameFilePath(filePath) == null)
97103
return NotFound();
98104

99105
if (WebGLCache.TryGetValue(filePath, out var cached))
@@ -123,14 +129,16 @@ public ActionResult GetEmoteWebGLData([FromQuery] string filePath)
123129
var decompileProc = Process.Start(new ProcessStartInfo
124130
{
125131
FileName = decompilePath,
126-
Arguments = $"\"{tempEmtbytes}\"",
127132
UseShellExecute = false,
128133
WorkingDirectory = tempDir,
129134
RedirectStandardOutput = true,
130135
RedirectStandardError = true,
131136
CreateNoWindow = true,
137+
ArgumentList = { tempEmtbytes },
132138
});
133139
decompileProc?.WaitForExit(30000);
140+
if (decompileProc == null || decompileProc.ExitCode != 0)
141+
return BadRequest($"PsbDecompile 失败,退出码: {decompileProc?.ExitCode ?? -1}");
134142

135143
var jsonPath = Path.Combine(tempDir, baseName + ".json");
136144
if (!System.IO.File.Exists(jsonPath))
@@ -145,14 +153,16 @@ public ActionResult GetEmoteWebGLData([FromQuery] string filePath)
145153
var buildProc = Process.Start(new ProcessStartInfo
146154
{
147155
FileName = buildPath,
148-
Arguments = $"-p ems -o \"{outputPath}\" \"{jsonPath}\"",
149156
UseShellExecute = false,
150157
WorkingDirectory = tempDir,
151158
RedirectStandardOutput = true,
152159
RedirectStandardError = true,
153160
CreateNoWindow = true,
161+
ArgumentList = { "-p", "ems", "-o", outputPath, jsonPath },
154162
});
155163
buildProc?.WaitForExit(30000);
164+
if (buildProc == null || buildProc.ExitCode != 0)
165+
return BadRequest($"PsBuild 失败,退出码: {buildProc?.ExitCode ?? -1}");
156166

157167
if (!System.IO.File.Exists(outputPath))
158168
return BadRequest("PsBuild 失败:未生成 pure.psb 文件");

ChuChartManager/PathGuard.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace ChuChartManager;
2+
3+
public static class PathGuard
4+
{
5+
/// <summary>
6+
/// 验证 userPath 是否在 allowedBaseDir 范围内,防止路径遍历。
7+
/// 返回规范化后的全路径;越界时返回 null。
8+
/// </summary>
9+
public static string? EnsureWithin(string allowedBaseDir, string userPath)
10+
{
11+
var fullBase = Path.GetFullPath(allowedBaseDir);
12+
var fullTarget = Path.GetFullPath(userPath);
13+
return fullTarget.StartsWith(fullBase, StringComparison.OrdinalIgnoreCase) ? fullTarget : null;
14+
}
15+
16+
/// <summary>
17+
/// 验证 userPath 在 allowedBaseDir 范围内,是存在的文件。
18+
/// </summary>
19+
public static bool FileExistsWithin(string allowedBaseDir, string userPath, out string safePath)
20+
{
21+
safePath = "";
22+
var resolved = EnsureWithin(allowedBaseDir, userPath);
23+
if (resolved == null || !File.Exists(resolved)) return false;
24+
safePath = resolved;
25+
return true;
26+
}
27+
}

ChuChartManager/ServerManager.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,18 @@ public static void StartApp(bool export = false, Action<string>? onStart = null)
6565
{
6666
serverOptions.Limits.MaxRequestBodySize = null;
6767
serverOptions.Listen(IPAddress.Loopback, 0);
68+
#if !DEBUG
6869
if (export)
6970
{
70-
serverOptions.Listen(IPAddress.Any, 5001);
71+
serverOptions.Listen(IPAddress.Any, 5001, listenOptions =>
72+
{
73+
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
74+
{
75+
ServerCertificate = GetCert()
76+
});
77+
});
7178
}
79+
#endif
7280
});
7381

7482
builder.Services
@@ -114,6 +122,7 @@ public static void StartApp(bool export = false, Action<string>? onStart = null)
114122
{
115123
App.UseAuthentication();
116124
App.UseAuthorization();
125+
App.UseMiddleware<AuthenticationMiddleware>();
117126
}
118127

119128
App

0 commit comments

Comments
 (0)