From 46255720ee8c416c8a0f7dbf129a8897bd9e4eaa Mon Sep 17 00:00:00 2001 From: OoO Date: Mon, 4 May 2026 14:23:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(p35):=20Critic=20HIGH=20#2=20+=20MEDIUM=20#?= =?UTF-8?q?2=20=E2=80=94=20SQL=20f-string=20+=20=E5=8B=95=E6=85=8B=20impor?= =?UTF-8?q?t=20=E6=94=B9=E5=AF=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HIGH #2 — ai_calls 動態 WHERE 從 f-string 拼接改全綁參數: 舊:sa_text(f"WHERE {' AND '.join(where_parts)}") 新:sa_text("WHERE :since AND (:caller_f='' OR caller=:caller_f) AND ...") 原本字串字面值來源安全,但下個 contributor 不慎把 request.args 拼進去 就立即 SQL injection;改全綁參數消除類別風險。 MEDIUM #2 — ppt_audit_history 動態 __import__ 改頂部 import: 舊:__import__('time').time() / __import__('datetime').datetime.fromtimestamp(...) 新:頂部 import time(datetime 已有)+ 直接呼叫 並新增 os.path.islink() 過濾,防 reports/ 內 symlink 攻擊逃出目錄。 12/12 tests 仍 PASS。 --- routes/admin_observability_routes.py | 32 +++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index e0b92b8..25834af 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -79,27 +79,25 @@ def ai_calls_dashboard(): {'since': since}, ).fetchall() - # 3. TOP 20 calls(最近)— 動態 WHERE - where_parts = ["called_at >= :since"] - params = {'since': since} - if caller_filter: - where_parts.append("caller = :caller") - params['caller'] = caller_filter - if provider_filter: - where_parts.append("provider = :provider") - params['provider'] = provider_filter - + # 3. TOP 100 calls — Phase 33 Critic HIGH #2 修補: + # 改用固定 SQL + 全綁參數,移除 f-string 動態 WHERE 拼接(防後人不慎注入) recent = session.execute( - sa_text(f""" + sa_text(""" SELECT id, called_at, caller, provider, model, input_tokens, output_tokens, duration_ms, status, cost_usd, cache_hit, rag_hit FROM ai_calls - WHERE {' AND '.join(where_parts)} + WHERE called_at >= :since + AND (:caller_f = '' OR caller = :caller_f) + AND (:provider_f = '' OR provider = :provider_f) ORDER BY called_at DESC LIMIT 100 """), - params, + { + 'since': since, + 'caller_f': caller_filter, + 'provider_f': provider_filter, + }, ).fetchall() # 4. caller 列表(給篩選 dropdown) @@ -369,6 +367,7 @@ def budget_update(budget_id: int): def ppt_audit_history(): """掃 reports/ 目錄列近 7 日 .pptx 檔 + 即時跑 audit(如已啟用)""" import os + import time reports_dir = 'reports' files = [] error = None @@ -377,18 +376,21 @@ def ppt_audit_history(): if not os.path.isdir(reports_dir): error = f'{reports_dir} 目錄不存在' else: - cutoff = __import__('time').time() - 7 * 86400 + cutoff = time.time() - 7 * 86400 for f in os.listdir(reports_dir): if not f.lower().endswith('.pptx'): continue full = os.path.join(reports_dir, f) + # symlink 防護:reports/ 內不接受 symlink,避免目錄逃逸(Critic MEDIUM #2) + if os.path.islink(full): + continue try: mtime = os.path.getmtime(full) if mtime >= cutoff: files.append({ 'name': f, 'size_kb': round(os.path.getsize(full) / 1024, 1), - 'mtime': __import__('datetime').datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M'), + 'mtime': datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M'), 'mtime_ts': mtime, }) except OSError: