fix: security, data-integrity, and doc-drift findings from repo audit
Consolidates duplicate KIS API client implementations (governance tests were exercising an unused class instead of the one actually running in production), closes a SQL injection path in the DB admin page, fixes a migration that used MySQL-only syntax and had never actually applied (confirmed against production), resyncs docs/db/quantengine.dbml with all migrations, and removes a duplicate OMS·WMS·ERP frontend tree in favor of src/frontend/. Also corrects several unverifiable/inflated claims in the OMS planning docs and realigns CI/CD and architecture documentation with what's actually in the repo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,18 +75,32 @@ namespace QuantEngine.Web.Pages.Admin.Database
|
||||
using var conn = _connectionFactory.CreateConnection();
|
||||
if (conn.State != ConnectionState.Open) conn.Open();
|
||||
|
||||
// Column/PK names come from the request, so they must be checked against the
|
||||
// table's real columns before going anywhere near a SQL string — otherwise an
|
||||
// attacker-controlled form field name reaches the query unescaped.
|
||||
var columnWhitelist = await LoadColumnWhitelistAsync(conn, tableName);
|
||||
|
||||
if (string.IsNullOrEmpty(pkColumn) || !columnWhitelist.Contains(pkColumn))
|
||||
{
|
||||
ErrorMessage = "허용되지 않은 기본 키 컬럼입니다.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
// Load target columns to update
|
||||
var columns = new List<string>();
|
||||
var parameters = new List<NpgsqlParameter>();
|
||||
|
||||
|
||||
foreach (var key in Request.Form.Keys)
|
||||
{
|
||||
if (key == "tableName" || key == "pkColumn" || key == "pkValue" || key == "__RequestVerificationToken")
|
||||
continue;
|
||||
|
||||
if (!columnWhitelist.Contains(key))
|
||||
continue; // 테이블에 실재하지 않는 컬럼명은 무시 (SQL 인젝션 방지)
|
||||
|
||||
var val = Request.Form[key].ToString();
|
||||
columns.Add($"\"{key}\" = @{key}");
|
||||
|
||||
|
||||
var param = new NpgsqlParameter($"@{key}", NpgsqlTypes.NpgsqlDbType.Text);
|
||||
param.Value = (object?)val ?? DBNull.Value;
|
||||
parameters.Add(param);
|
||||
@@ -133,6 +147,8 @@ namespace QuantEngine.Web.Pages.Admin.Database
|
||||
using var conn = _connectionFactory.CreateConnection();
|
||||
if (conn.State != ConnectionState.Open) conn.Open();
|
||||
|
||||
var columnWhitelist = await LoadColumnWhitelistAsync(conn, tableName);
|
||||
|
||||
var colNames = new List<string>();
|
||||
var paramNames = new List<string>();
|
||||
var parameters = new List<NpgsqlParameter>();
|
||||
@@ -142,6 +158,9 @@ namespace QuantEngine.Web.Pages.Admin.Database
|
||||
if (key == "tableName" || key == "__RequestVerificationToken")
|
||||
continue;
|
||||
|
||||
if (!columnWhitelist.Contains(key))
|
||||
continue; // 테이블에 실재하지 않는 컬럼명은 무시 (SQL 인젝션 방지)
|
||||
|
||||
var val = Request.Form[key].ToString();
|
||||
colNames.Add($"\"{key}\"");
|
||||
paramNames.Add($"@{key}");
|
||||
@@ -171,6 +190,35 @@ namespace QuantEngine.Web.Pages.Admin.Database
|
||||
return RedirectToPage(new { tableName });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DB-verified column names for a table, used to validate any identifier (PK column,
|
||||
/// form field names) before it is interpolated into a SQL string. Table names arriving
|
||||
/// here must already be whitelist-checked against TableList by the caller.
|
||||
/// </summary>
|
||||
private async Task<HashSet<string>> LoadColumnWhitelistAsync(IDbConnection conn, string tableName)
|
||||
{
|
||||
var parts = tableName.Split('.');
|
||||
var schema = parts[0];
|
||||
var tableOnly = parts[1];
|
||||
|
||||
var sql = @"
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @schema AND table_name = @table_only;";
|
||||
|
||||
using var cmd = new NpgsqlCommand(sql, (NpgsqlConnection)conn);
|
||||
cmd.Parameters.AddWithValue("@schema", schema);
|
||||
cmd.Parameters.AddWithValue("@table_only", tableOnly);
|
||||
|
||||
var columns = new HashSet<string>(StringComparer.Ordinal);
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
columns.Add(reader.GetString(0));
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
private async Task LoadTableListAsync()
|
||||
{
|
||||
TableList.Clear();
|
||||
|
||||
Reference in New Issue
Block a user