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:
@@ -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 = ['總業績', '數量', '總成本', '退貨數量', '商品單位售價',
|
||||
|
||||
Reference in New Issue
Block a user