security: address HIGH findings (stored XSS, wildcard CORS, prod log leak)
H1 — stored XSS via v-html / innerHTML of unsanitized LLM/agent/interview/report content: add DOMPurify; both renderMarkdown() now return DOMPurify.sanitize(html) (Step4Report.vue, Step5Interaction.vue), and the formatAnswer innerHTML sink is wrapped in DOMPurify.sanitize. All 8 HTML-injection sinks now sanitized; markdown rendering preserved (DOMPurify secure defaults keep the md-* tags/classes). H4 — wildcard CORS: CORS origins now Config.ALLOWED_ORIGINS (comma-separated env, default localhost:3000) instead of '*'. H5 — request bodies written to disk in cleartext: logger file level now follows FLASK_DEBUG (INFO in prod), so the before_request body-debug log is suppressed in production. (traceback-in-response across 53 handlers deferred to a separate refactor.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8cb92768ae
commit
394638d426
|
|
@ -30,6 +30,10 @@ API_KEY=change_me_to_a_strong_api_key
|
|||
# 使打包后的 UI 自动带上 X-API-Key。注意:它会被打进客户端包、可被任何访问者提取(见 README 安全说明)。
|
||||
VITE_API_KEY=change_me_to_a_strong_api_key
|
||||
|
||||
# ===== CORS 允许来源(H4)=====
|
||||
# 逗号分隔的前端来源;不再用通配 '*'。生产填前端域名,例如 https://app.example.com
|
||||
ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
||||
|
||||
# ===== 模拟成本上限(C3,denial-of-wallet 防护)=====
|
||||
# 客户端未传 max_rounds 时的默认轮数上限(完整长度模拟请按请求传 max_rounds 或调高此值)
|
||||
# 默认 150 覆盖典型配置(72h/30min=144 轮)以免悄悄截断标准演示
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ def create_app(config_class=Config):
|
|||
logger.info("MiroFish Backend 启动中...")
|
||||
logger.info("=" * 50)
|
||||
|
||||
# 启用CORS
|
||||
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
||||
# 启用CORS(H4):限定来源为 Config.ALLOWED_ORIGINS(默认本地前端源),不再通配 '*'。
|
||||
CORS(app, resources={r"/api/*": {"origins": Config.ALLOWED_ORIGINS}})
|
||||
|
||||
# API Key 鉴权(C2):所有 /api/* 端点强制鉴权。
|
||||
# 客户端通过 `X-API-Key: <key>` 或 `Authorization: Bearer <key>` 传入。
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@ class Config:
|
|||
API_KEY = os.environ.get('API_KEY')
|
||||
AUTH_ENABLED = os.environ.get('AUTH_ENABLED', 'true').strip().lower() not in ('false', '0', 'no', 'off')
|
||||
|
||||
# CORS 允许来源(H4):不再用通配 '*'。默认仅本地前端开发/预览源;生产用逗号分隔的
|
||||
# ALLOWED_ORIGINS 指定前端域名(例如 https://app.example.com)。'*' 仍可显式设置但不推荐。
|
||||
ALLOWED_ORIGINS = [
|
||||
o.strip() for o in os.environ.get(
|
||||
'ALLOWED_ORIGINS', 'http://localhost:3000,http://127.0.0.1:3000'
|
||||
).split(',') if o.strip()
|
||||
]
|
||||
|
||||
# JSON配置 - 禁用ASCII转义,让中文直接显示(而不是 \uXXXX 格式)
|
||||
JSON_AS_ASCII = False
|
||||
|
||||
|
|
|
|||
|
|
@ -27,17 +27,23 @@ def _ensure_utf8_stdout():
|
|||
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'logs')
|
||||
|
||||
|
||||
def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.Logger:
|
||||
def setup_logger(name: str = 'mirofish', level: int = None) -> logging.Logger:
|
||||
"""
|
||||
设置日志器
|
||||
|
||||
|
||||
Args:
|
||||
name: 日志器名称
|
||||
level: 日志级别
|
||||
|
||||
level: 日志级别(None 时按 FLASK_DEBUG 自动选择)
|
||||
|
||||
Returns:
|
||||
配置好的日志器
|
||||
"""
|
||||
# H5:生产模式(FLASK_DEBUG=false)下用 INFO —— 避免把请求体/调试细节(可能含上传内容、
|
||||
# 提示词、客户端传入的凭据)以明文写入轮转日志文件。开发模式仍用 DEBUG。
|
||||
if level is None:
|
||||
from ..config import Config
|
||||
level = logging.DEBUG if Config.DEBUG else logging.INFO
|
||||
|
||||
# 确保日志目录存在
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
|
|
@ -71,7 +77,7 @@ def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.
|
|||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setLevel(level)
|
||||
file_handler.setFormatter(detailed_formatter)
|
||||
|
||||
# 2. 控制台处理器 - 简洁日志(INFO及以上)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"axios": "^1.14.0",
|
||||
"d3": "^7.9.0",
|
||||
"dompurify": "^3.4.10",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.3.0",
|
||||
"vue-router": "^4.6.3"
|
||||
|
|
@ -938,6 +939,13 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.2.tgz",
|
||||
|
|
@ -1435,7 +1443,6 @@
|
|||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -1538,6 +1545,15 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.10",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz",
|
||||
"integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
|
@ -1913,7 +1929,6 @@
|
|||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -2053,7 +2068,6 @@
|
|||
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -2128,7 +2142,6 @@
|
|||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
|
||||
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-sfc": "3.5.25",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"axios": "^1.14.0",
|
||||
"d3": "^7.9.0",
|
||||
"dompurify": "^3.4.10",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.3.0",
|
||||
"vue-router": "^4.6.3"
|
||||
|
|
|
|||
|
|
@ -391,6 +391,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick, h, reactive } from 'vue'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getAgentLog, getConsoleLog } from '../api/report'
|
||||
|
|
@ -1531,11 +1532,14 @@ const InterviewDisplay = {
|
|||
]),
|
||||
h('div', {
|
||||
class: ['qa-text', 'answer-text', { 'placeholder-text': isPlaceholder }],
|
||||
innerHTML: isPlaceholder
|
||||
? answerText
|
||||
: formatAnswer(answerText, isExpanded)
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\n/g, '<br>')
|
||||
// H1:answerText 来自不可信 LLM 回答,消毒后再入 innerHTML,防存储型 XSS
|
||||
innerHTML: DOMPurify.sanitize(
|
||||
isPlaceholder
|
||||
? answerText
|
||||
: formatAnswer(answerText, isExpanded)
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\n/g, '<br>')
|
||||
)
|
||||
}),
|
||||
// Expand/Collapse Button(占位文本不显示)
|
||||
!isPlaceholder && answerText.length > 400 && h('button', {
|
||||
|
|
@ -1973,7 +1977,8 @@ const renderMarkdown = (content) => {
|
|||
}
|
||||
html = tokens.join('')
|
||||
|
||||
return html
|
||||
// H1:消毒最终 HTML,剥离来自不可信 LLM 报告内容的脚本/事件处理器,防止存储型 XSS
|
||||
return DOMPurify.sanitize(html)
|
||||
}
|
||||
|
||||
const getTimelineItemClass = (log, idx, total) => {
|
||||
|
|
|
|||
|
|
@ -412,6 +412,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { chatWithReport, getReport, getAgentLog } from '../api/report'
|
||||
import { interviewAgents, getSimulationProfilesRealtime } from '../api/simulation'
|
||||
|
|
@ -638,7 +639,8 @@ const renderMarkdown = (content) => {
|
|||
}
|
||||
html = tokens.join('')
|
||||
|
||||
return html
|
||||
// H1:消毒最终 HTML,剥离来自不可信 LLM/采访内容的脚本/事件处理器,防止存储型 XSS
|
||||
return DOMPurify.sanitize(html)
|
||||
}
|
||||
|
||||
// Chat Methods
|
||||
|
|
|
|||
Loading…
Reference in New Issue