security: P0 修復 S1-S5 — 移除所有硬編碼密碼與 SQL Injection 漏洞

S1: config.py — LOGIN_PASSWORD 移除硬編碼預設值 0936223270,改 fail-fast
S2: config.py — SECRET_KEY 移除弱預設值,無值或預設值時 sys.exit(1)
S3: services/user_service.py — create_initial_admin 改讀 INITIAL_ADMIN_PASSWORD env
S4: app.py — 匯入流程 table_name 正規表達式白名單驗證,date_list 格式驗證
S5: database/manager.py — ALLOWED_SALES_TABLES frozenset 白名單,日期改參數化查詢

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ogt
2026-04-27 20:34:15 +08:00
parent b3a7909b2b
commit f59b23f969
4 changed files with 78 additions and 19 deletions

View File

@@ -275,6 +275,18 @@ class DatabaseManager:
if not external_session:
session.close()
# S5 安全修復table_name 白名單,防止 SQL Injection
ALLOWED_SALES_TABLES = frozenset({
'realtime_sales_monthly',
'daily_sales_snapshot',
'monthly_summary_analysis',
'vendor_performance',
'daily_performance',
'edm_products',
'festival_products',
'competitor_prices',
})
def get_sales_data(self, table_name='realtime_sales_monthly', start_date=None, end_date=None, months=None):
"""
從指定的銷售資料表中讀取資料
@@ -290,23 +302,35 @@ class DatabaseManager:
"""
import pandas as pd
from datetime import datetime, timedelta
import re as _re_sec
# S5 白名單驗證:只允許已知的銷售資料表
if table_name not in self.ALLOWED_SALES_TABLES:
raise ValueError(f"[Security] 非法的銷售資料表名稱:{table_name!r}。允許的表名:{sorted(self.ALLOWED_SALES_TABLES)}")
try:
# 建立日期過濾條件
# 建立日期過濾條件(使用參數化查詢防止日期注入)
date_filter = ""
params = {}
if start_date and end_date:
date_filter = f" WHERE \"日期\" BETWEEN '{start_date}' AND '{end_date}'"
# 格式驗證:只允許 YYYY-MM-DD 格式
if not _re_sec.match(r'^\d{4}-\d{2}-\d{2}$', str(start_date)) or \
not _re_sec.match(r'^\d{4}-\d{2}-\d{2}$', str(end_date)):
raise ValueError(f"日期格式不合法start={start_date}, end={end_date}")
date_filter = " WHERE \"日期\" BETWEEN :start_date AND :end_date"
params = {'start_date': str(start_date), 'end_date': str(end_date)}
elif months:
# 計算 months 個月前的日期
end_dt = datetime.now()
start_dt = end_dt - timedelta(days=months * 30)
start_date_str = start_dt.strftime('%Y-%m-%d')
end_date_str = end_dt.strftime('%Y-%m-%d')
date_filter = f" WHERE \"日期\" BETWEEN '{start_date_str}' AND '{end_date_str}'"
date_filter = " WHERE \"日期\" BETWEEN :start_date AND :end_date"
params = {'start_date': start_date_str, 'end_date': end_date_str}
# 執行查詢
# 執行查詢table_name 已白名單驗證,日期已參數化)
sql = f"SELECT * FROM {table_name}{date_filter}"
df = pd.read_sql(text(sql), self.engine)
df = pd.read_sql(text(sql), self.engine, params=params if params else None)
# V-Fix: 將數值欄位轉換為數字類型
numeric_columns = ['總業績', '數量', '總成本', '退貨數量', '商品單位售價',