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:
2026-07-30 11:20:02 +09:00
parent 99943d9871
commit 70824c2afb
185 changed files with 2062 additions and 34521 deletions
@@ -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();