From dea94d2e0fb3a896bbb5390b0088d04c2d4780c8 Mon Sep 17 00:00:00 2001 From: ooo Date: Tue, 28 Apr 2026 15:50:21 +0800 Subject: [PATCH] =?UTF-8?q?refactor(p1-01d):=20routes/=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=20safe=5Fread=5Fsql/validate=5Ftable=5Fname/find=5Fco?= =?UTF-8?q?l=20=E4=B8=89=E4=BB=BD=E9=87=8D=E8=A4=87=E5=AE=9A=E7=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - routes/sales_routes.py 移除 find_col、validate_table_name、safe_read_sql 各自實作(-40 行) - routes/daily_sales_routes.py 移除 validate_table_name、safe_read_sql 各自實作(-26 行) - 兩檔改為 from utils.security import ... 的 re-export,行為對齊單一權威來源 注意:原本 routes 自己的 validate_table_name 較寬鬆(只 regex), 改用 utils.security 後升級為「白名單 + SQL 關鍵字」雙重防護。 所有 call site 都用 'realtime_sales_monthly' 或 'daily_sales_snapshot',皆在白名單內,行為相容。 --- routes/daily_sales_routes.py | 30 ++------------------------- routes/sales_routes.py | 40 +++--------------------------------- 2 files changed, 5 insertions(+), 65 deletions(-) diff --git a/routes/daily_sales_routes.py b/routes/daily_sales_routes.py index 929f1ed..d30111a 100644 --- a/routes/daily_sales_routes.py +++ b/routes/daily_sales_routes.py @@ -74,34 +74,8 @@ def _is_cache_valid(cache_key): # 輔助函數 # ========================================== -def validate_table_name(table_name): - """驗證表名(防止 SQL Injection)""" - import re - if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', table_name): - raise ValueError(f"Invalid table name: {table_name}") - return table_name - - -def safe_read_sql(table_name, columns=None, engine=None, where_clause=None, limit=None, params=None): - """安全的 SQL 查詢函數,防止 SQL Injection""" - table_name = validate_table_name(table_name) - - if columns: - col_str = ', '.join([f'"{col}"' for col in columns]) - else: - col_str = '*' - - try: - query = f'SELECT {col_str} FROM "{table_name}"' - if where_clause: - query += f' WHERE {where_clause}' - if limit: - query += f' LIMIT {int(limit)}' - - return pd.read_sql(text(query), engine, params=params) - except Exception as e: - sys_log.error(f"[Security] SQL 查詢失敗: {e}") - raise +# 共用工具改 import 自 utils(去重,原本檔案內定義已移除) +from utils.security import validate_table_name, safe_read_sql # noqa: E402, F401 def preprocess_daily_sales_data(df): diff --git a/routes/sales_routes.py b/routes/sales_routes.py index 7f4997a..aeed001 100644 --- a/routes/sales_routes.py +++ b/routes/sales_routes.py @@ -38,43 +38,9 @@ _SALES_OPTIONS_CACHE = {} # 輔助函數 # ========================================== -def find_col(df_cols, keywords): - """從欄位列表中,根據關鍵字列表找出最匹配的欄位名稱""" - for k in keywords: - for col in df_cols: - if k in str(col): - return col - return None - - -def validate_table_name(table_name): - """驗證表名(防止 SQL Injection)""" - import re - if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', table_name): - raise ValueError(f"Invalid table name: {table_name}") - return table_name - - -def safe_read_sql(table_name, columns=None, engine=None, where_clause=None, limit=None, params=None): - """安全的 SQL 查詢函數,防止 SQL Injection""" - table_name = validate_table_name(table_name) - - if columns: - col_str = ', '.join([f'"{col}"' for col in columns]) - else: - col_str = '*' - - try: - query = f'SELECT {col_str} FROM "{table_name}"' - if where_clause: - query += f' WHERE {where_clause}' - if limit: - query += f' LIMIT {int(limit)}' - - return pd.read_sql(text(query), engine, params=params) - except Exception as e: - sys_log.error(f"[Security] SQL 查詢失敗: {e}") - raise +# 共用工具改 import 自 utils(去重,原各 routes 各自定義已移除) +from utils.df_helpers import find_col # noqa: E402, F401 +from utils.security import validate_table_name, safe_read_sql # noqa: E402, F401 def _get_filtered_sales_data(cache_key):