Skip to content

Commit ce8b375

Browse files
fix: Embedded security vulnerabilities
1 parent ee5e80d commit ce8b375

7 files changed

Lines changed: 164 additions & 925 deletions

File tree

backend/apps/system/api/assistant.py

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import os
3-
from datetime import timedelta
3+
from datetime import datetime, timedelta, timezone
4+
from zoneinfo import ZoneInfo
45
from typing import List, Optional
56

67
from fastapi import APIRouter, Form, HTTPException, Path, Query, Request, Response, UploadFile
@@ -22,30 +23,83 @@
2223
from common.core.security import create_access_token
2324
from common.core.sqlbot_cache import clear_cache
2425
from common.utils.utils import get_origin_from_referer, origin_match_domain
25-
2626
router = APIRouter(tags=["system_assistant"], prefix="/system/assistant")
2727
from common.audit.models.log_model import OperationType, OperationModules
2828
from common.audit.schemas.logger_decorator import LogConfig, system_log
29-
29+
from sqlbot_xpack.core import decrypt_embedded_sign
3030

3131
@router.get("/info/{id}", include_in_schema=False)
32-
async def info(request: Request, response: Response, session: SessionDep, trans: Trans, id: int) -> AssistantModel:
32+
async def info(request: Request, response: Response, session: SessionDep, trans: Trans, id: int, virtual: Optional[int] = Query(None)):
3333
if not id:
3434
raise Exception('miss assistant id')
3535
db_model = await get_assistant_info(session=session, assistant_id=id)
3636
if not db_model:
3737
raise RuntimeError(f"assistant application not exist")
3838
db_model = AssistantModel.model_validate(db_model)
39-
40-
origin = request.headers.get("origin") or get_origin_from_referer(request)
41-
if not origin:
42-
raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=origin or ''))
43-
origin = origin.rstrip('/')
44-
if not origin_match_domain(origin, db_model.domain):
45-
raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=origin or ''))
46-
39+
40+
# 校验 SQLBOT-EMBEDDED-SIGN 请求头
41+
sign_header = request.headers.get("SQLBOT-EMBEDDED-SIGN")
42+
if not sign_header:
43+
raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=''))
44+
45+
sign_data = await decrypt_embedded_sign(sign_header)
46+
47+
# 校验 assistant_id 与 id 参数一致
48+
if str(sign_data.get("assistant_id")) != str(id):
49+
raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=''))
50+
51+
# 校验 target(来源域名)是否合法
52+
target = sign_data.get("target", "")
53+
if not origin_match_domain(target, db_model.domain):
54+
raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=target or ''))
55+
56+
# 校验 sign_time 是否在 10 秒内
57+
sign_time_str = sign_data.get("sign_time", "")
58+
sign_time = datetime.fromisoformat(sign_time_str)
59+
now_utc = datetime.now(timezone.utc)
60+
sign_time_utc = sign_time.astimezone(timezone.utc)
61+
if abs((now_utc - sign_time_utc).total_seconds()) > 10:
62+
raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=target or ''))
63+
64+
# 校验是否为真实浏览器请求(非自动化工具)
65+
if sign_data.get("webdriver", False):
66+
raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=target or ''))
67+
68+
# 校验 User-Agent 一致性(签名中的 navigator.userAgent 与请求头一致)
69+
sign_user_agent = sign_data.get("user_agent", "")
70+
request_user_agent = request.headers.get("User-Agent", "")
71+
if sign_user_agent != request_user_agent:
72+
raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=target or ''))
73+
74+
# 校验 timezone 与 sign_time 偏移一致性(防时区伪造)
75+
tz_name = sign_data.get("timezone", "")
76+
tz = ZoneInfo(tz_name)
77+
sign_time_naive = sign_time.replace(tzinfo=None)
78+
if tz.utcoffset(sign_time_naive) != sign_time.utcoffset():
79+
raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=target or ''))
80+
81+
origin = target.rstrip('/')
82+
4783
response.headers["Access-Control-Allow-Origin"] = origin
48-
return db_model
84+
85+
86+
assistant_oid = 1
87+
if (db_model.type == 0):
88+
configuration = db_model.configuration
89+
config_obj = json.loads(configuration) if configuration else {}
90+
assistant_oid = config_obj.get('oid', 1)
91+
92+
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
93+
assistantDict = {
94+
"id": virtual, "account": 'sqlbot-inner-assistant', "oid": assistant_oid, "assistant_id": id
95+
}
96+
access_token = create_access_token(
97+
assistantDict, expires_delta=access_token_expires
98+
)
99+
100+
result = db_model.model_dump()
101+
result["token"] = access_token
102+
return result
49103

50104

51105
@router.get("/app/{appId}", include_in_schema=False)
@@ -67,7 +121,7 @@ async def getApp(request: Request, response: Response, session: SessionDep, tran
67121
return db_model
68122

69123

70-
@router.get("/validator", response_model=AssistantValidator, include_in_schema=False)
124+
""" @router.get("/validator", response_model=AssistantValidator, include_in_schema=False)
71125
async def validator(session: SessionDep, id: int, virtual: Optional[int] = Query(None)):
72126
if not id:
73127
raise Exception('miss assistant id')
@@ -89,7 +143,7 @@ async def validator(session: SessionDep, id: int, virtual: Optional[int] = Query
89143
access_token = create_access_token(
90144
assistantDict, expires_delta=access_token_expires
91145
)
92-
return AssistantValidator(True, True, True, access_token)
146+
return AssistantValidator(True, True, True, access_token) """
93147

94148

95149
@router.get('/picture/{file_id}', summary=f"{PLACEHOLDER_PREFIX}assistant_picture_api", description=f"{PLACEHOLDER_PREFIX}assistant_picture_api")

backend/apps/system/crud/assistant.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@
2222
from common.core.response_middleware import ResponseMiddleware
2323

2424

25+
def _update_cors_middleware_instance(app: FastAPI, updated_origins: list[str]):
26+
"""遍历 middleware 栈,找到 CORSMiddleware 实例并更新其 allow_origins。
27+
28+
仅修改 middleware.kwargs 不会影响已构建的中间件实例,
29+
需要直接更新实例的 allow_origins 属性。
30+
"""
31+
stack = getattr(app, 'middleware_stack', None)
32+
while stack is not None and hasattr(stack, 'app'):
33+
if isinstance(stack, CORSMiddleware):
34+
stack.allow_origins = updated_origins
35+
return
36+
stack = stack.app
37+
38+
39+
2540
@cache(namespace=CacheNamespace.EMBEDDED_INFO, cacheName=CacheName.ASSISTANT_INFO, keyExpression="assistant_id")
2641
async def get_assistant_info(*, session: Session, assistant_id: int) -> AssistantModel | None:
2742
db_model = session.get(AssistantModel, assistant_id)
@@ -98,6 +113,7 @@ def init_dynamic_cors(app: FastAPI):
98113
updated_origins = list(set(settings.all_cors_origins + unique_domains))
99114
if cors_middleware:
100115
cors_middleware.kwargs['allow_origins'] = updated_origins
116+
_update_cors_middleware_instance(app, updated_origins)
101117
if response_middleware:
102118
for instance in ResponseMiddleware.instances:
103119
instance.update_allow_origins(updated_origins)

backend/apps/system/crud/assistant_manage.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@
1212
from common.core.response_middleware import ResponseMiddleware
1313

1414

15+
def _update_cors_middleware_instance(app: FastAPI, updated_origins: list[str]):
16+
"""遍历 middleware 栈,找到 CORSMiddleware 实例并更新其 allow_origins。"""
17+
stack = getattr(app, 'middleware_stack', None)
18+
while stack is not None and hasattr(stack, 'app'):
19+
if isinstance(stack, CORSMiddleware):
20+
stack.allow_origins = updated_origins
21+
return
22+
stack = stack.app
23+
24+
25+
1526
def dynamic_upgrade_cors(request: Request, session: Session):
1627
list_result = session.exec(select(AssistantModel).order_by(AssistantModel.create_time)).all()
1728
seen = set()
@@ -37,6 +48,7 @@ def dynamic_upgrade_cors(request: Request, session: Session):
3748
updated_origins = list(set(settings.all_cors_origins + unique_domains))
3849
if cors_middleware:
3950
cors_middleware.kwargs['allow_origins'] = updated_origins
51+
_update_cors_middleware_instance(app, updated_origins)
4052
if response_middleware:
4153
for instance in ResponseMiddleware.instances:
4254
instance.update_allow_origins(updated_origins)

backend/apps/system/middleware/auth.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11

22
import base64
33
import json
4+
import re
45
from typing import Optional
56
from fastapi import Request
67
from fastapi.responses import JSONResponse
8+
from starlette.responses import Response
79
import jwt
810
from sqlmodel import Session
911
from starlette.middleware.base import BaseHTTPMiddleware
1012
from apps.system.crud.apikey_manage import get_api_key
1113
from apps.system.models.system_model import ApiKeyModel, AssistantModel
12-
from common.core.db import engine
14+
from common.core.db import engine
1315
from apps.system.crud.assistant import get_assistant_info, get_assistant_user
1416
from apps.system.crud.user import get_user_by_account, get_user_info
1517
from apps.system.schemas.system_schema import AssistantHeader, UserInfoDTO
1618
from common.core import security
1719
from common.core.config import settings
1820
from common.core.schemas import TokenPayload
1921
from common.utils.locale import I18n
20-
from common.utils.utils import SQLBotLogUtil, get_origin_from_referer
22+
from common.utils.utils import SQLBotLogUtil, get_origin_from_referer, origin_match_domain
2123
from common.utils.whitelist import whiteUtils
2224
from fastapi.security.utils import get_authorization_scheme_param
2325
from common.core.deps import get_i18n
@@ -31,6 +33,26 @@ def __init__(self, app):
3133
async def dispatch(self, request, call_next):
3234

3335
if self.is_options(request) or whiteUtils.is_whitelisted(request.url.path):
36+
# 动态处理 /system/assistant/info/{id} 的 CORS 预检
37+
if request.method == "OPTIONS":
38+
origin = request.headers.get("origin", "")
39+
if origin:
40+
match = re.search(r'/system/assistant/info/(\d+)', request.url.path)
41+
if match:
42+
assistant_id = int(match.group(1))
43+
with Session(engine) as session:
44+
db_model = session.get(AssistantModel, assistant_id)
45+
if db_model and origin_match_domain(origin, db_model.domain):
46+
return Response(
47+
status_code=200,
48+
headers={
49+
"Access-Control-Allow-Origin": origin,
50+
"Access-Control-Allow-Methods": "GET, OPTIONS",
51+
"Access-Control-Allow-Headers": "*",
52+
"Access-Control-Allow-Credentials": "true",
53+
"Access-Control-Max-Age": "600",
54+
},
55+
)
3456
return await call_next(request)
3557
assistantTokenKey = settings.ASSISTANT_TOKEN_KEY
3658
assistantToken = request.headers.get(assistantTokenKey)

0 commit comments

Comments
 (0)