From 6d46fcf3785ea460f9c76a380eda69779a0c2882 Mon Sep 17 00:00:00 2001 From: LuoPengcheng <2653972504@qq.com> Date: Mon, 8 Dec 2025 19:14:22 +0800 Subject: [PATCH 1/8] fix Reflected server-side cross-site scripting --- server/app/controller/mcp/proxy_controller.py | 44 +++++++++++++++++-- .../app/controller/oauth/oauth_controller.py | 4 +- server/app/controller/redirect_controller.py | 3 +- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/server/app/controller/mcp/proxy_controller.py b/server/app/controller/mcp/proxy_controller.py index 0ec1a0cfd..8dc978692 100644 --- a/server/app/controller/mcp/proxy_controller.py +++ b/server/app/controller/mcp/proxy_controller.py @@ -20,6 +20,15 @@ def exa_search(search: ExaSearch, key: Key = Depends(key_must)): """Search using Exa API.""" EXA_API_KEY = env_not_empty("EXA_API_KEY") + secrets_to_redact = (EXA_API_KEY,) + + def _redact_secret(text: str) -> str: + redacted = text + for secret in secrets_to_redact: + if secret: + redacted = redacted.replace(secret, "[REDACTED]") + return redacted + try: # Validate input parameters if search.num_results is not None and not 0 < search.num_results <= 100: @@ -81,7 +90,11 @@ def exa_search(search: ExaSearch, key: Key = Depends(key_must)): logger.warning("Exa search validation error", extra={"error": str(e)}) raise HTTPException(status_code=500, detail="Internal server error") except Exception as e: - logger.error("Exa search failed", extra={"query": search.query, "error": str(e)}, exc_info=True) + logger.error( + "Exa search failed", + extra={"query": search.query, "error_type": type(e).__name__, "error": _redact_secret(str(e))}, + exc_info=False, + ) raise HTTPException(status_code=500, detail="Internal server error") @@ -93,6 +106,14 @@ def google_search(query: str, search_type: str = "web", key: Key = Depends(key_m GOOGLE_API_KEY = env_not_empty("GOOGLE_API_KEY") # https://cse.google.com/cse/all SEARCH_ENGINE_ID = env_not_empty("SEARCH_ENGINE_ID") + secrets_to_redact = (GOOGLE_API_KEY, SEARCH_ENGINE_ID) + + def _redact_secret(text: str) -> str: + redacted = text + for secret in secrets_to_redact: + if secret: + redacted = redacted.replace(secret, "[REDACTED]") + return redacted # Using the first page start_page_idx = 1 @@ -186,11 +207,28 @@ def google_search(query: str, search_type: str = "web", key: Key = Depends(key_m logger.info("Google search completed", extra={"query": query, "search_type": search_type, "result_count": len(responses)}) else: error_info = data.get("error", {}) - logger.error("Google search API error", extra={"query": query, "api_error": error_info}) + sanitized_error = { + "code": error_info.get("code"), + "reason": (error_info.get("errors") or [{}])[0].get("reason"), + "message": _redact_secret(error_info.get("message", "")), + } + logger.error( + "Google search API error", + extra={"query": query, "search_type": search_type, "api_error": sanitized_error}, + ) raise HTTPException(status_code=500, detail="Internal server error") except Exception as e: - logger.error("Google search failed", extra={"query": query, "search_type": search_type, "error": str(e)}, exc_info=True) + logger.error( + "Google search failed", + extra={ + "query": query, + "search_type": search_type, + "error_type": type(e).__name__, + "error": _redact_secret(str(e)), + }, + exc_info=False, + ) raise HTTPException(status_code=500, detail="Internal server error") return responses \ No newline at end of file diff --git a/server/app/controller/oauth/oauth_controller.py b/server/app/controller/oauth/oauth_controller.py index c43e50973..2bf891d2e 100644 --- a/server/app/controller/oauth/oauth_controller.py +++ b/server/app/controller/oauth/oauth_controller.py @@ -1,3 +1,4 @@ +import json from fastapi import APIRouter, Request, HTTPException from fastapi.responses import RedirectResponse, JSONResponse, HTMLResponse from app.component.environment import env @@ -46,6 +47,7 @@ def oauth_callback(app: str, request: Request, code: Optional[str] = None, state logger.info("OAuth callback received", extra={"provider": app, "has_state": state is not None}) redirect_url = f"eigent://callback/oauth?provider={app}&code={code}&state={state}" + safe_redirect_url = json.dumps(redirect_url) html_content = f""" @@ -53,7 +55,7 @@ def oauth_callback(app: str, request: Request, code: Optional[str] = None, state

Redirecting, please wait...

diff --git a/server/app/controller/redirect_controller.py b/server/app/controller/redirect_controller.py index 3695a8fb4..568b428c3 100644 --- a/server/app/controller/redirect_controller.py +++ b/server/app/controller/redirect_controller.py @@ -11,6 +11,7 @@ def redirect_callback(code: str, request: Request): cookies = request.cookies cookies_json = json.dumps(cookies) + safe_code = json.dumps(code) html_content = f""" @@ -59,7 +60,7 @@ def redirect_callback(code: str, request: Request): -

Redirecting, please wait...

- - - - """ - return HTMLResponse(content=html_content) + redirect_url = f"eigent://callback/oauth?{query}" + return RedirectResponse(redirect_url) @router.post("/{app}/token", name="OAuth Fetch Token") diff --git a/server/app/controller/redirect_controller.py b/server/app/controller/redirect_controller.py index 568b428c3..54121c382 100644 --- a/server/app/controller/redirect_controller.py +++ b/server/app/controller/redirect_controller.py @@ -1,7 +1,6 @@ -import json -from fastapi import APIRouter, Depends, Request -from fastapi_babel import _ -from fastapi.responses import HTMLResponse +from urllib.parse import urlencode, quote +from fastapi import APIRouter, Request +from fastapi.responses import RedirectResponse router = APIRouter(tags=["Redirect"]) @@ -9,65 +8,8 @@ @router.get("/redirect/callback") def redirect_callback(code: str, request: Request): - cookies = request.cookies - cookies_json = json.dumps(cookies) - safe_code = json.dumps(code) - html_content = f""" - - - - - - Authorization successful - - - -
-

Authorization Successful

-

Redirecting to application...

-
Please wait...
-
- - - - """ - return HTMLResponse(content=html_content) + params = {"code": code} + query = urlencode(params, quote_via=quote) + redirect_url = f"eigent://callback?{query}" + return RedirectResponse(redirect_url) \ No newline at end of file From a59c6d4f4d55a5923ff6adae809e64d634aafb70 Mon Sep 17 00:00:00 2001 From: LuoPengcheng <2653972504@qq.com> Date: Mon, 8 Dec 2025 23:33:07 +0800 Subject: [PATCH 5/8] remove redundant functions --- backend/app/model/chat.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/app/model/chat.py b/backend/app/model/chat.py index aaf23bddf..ee05e0fbf 100644 --- a/backend/app/model/chat.py +++ b/backend/app/model/chat.py @@ -81,11 +81,6 @@ def check_model_type(cls, model_type: str): logger.debug("model_type is invalid") return model_type - @field_validator("project_id", "task_id") - @classmethod - def check_path_component(cls, value: str, info): - return safe_component(value, info.field_name) - def get_bun_env(self) -> dict[str, str]: return {"NPM_CONFIG_REGISTRY": self.bun_mirror} if self.bun_mirror else {} From cf2d9b4b163f6ab39251eab032cf0ec1c67977cc Mon Sep 17 00:00:00 2001 From: LuoPengcheng <2653972504@qq.com> Date: Tue, 9 Dec 2025 00:11:14 +0800 Subject: [PATCH 6/8] minor update --- backend/app/component/environment.py | 2 +- server/app/controller/mcp/proxy_controller.py | 21 ++++++++++++------- .../app/controller/oauth/oauth_controller.py | 11 +++++++--- server/app/controller/redirect_controller.py | 5 ++++- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/backend/app/component/environment.py b/backend/app/component/environment.py index 5b3a350f4..ebed2b7c2 100644 --- a/backend/app/component/environment.py +++ b/backend/app/component/environment.py @@ -46,7 +46,7 @@ def set_user_env_path(env_path: str | None = None): delattr(_thread_local, 'env_path') traceroot_logger.info("Reset to default global environment") - if env_path and (not sanitized_path or not os.path.exists(env_path)): + if env_path and (not sanitized_path or not (sanitized_path and sanitized_path.exists())): traceroot_logger.warning( "User environment path does not exist or is invalid, falling back to global", extra={"env_path": env_path} ) diff --git a/server/app/controller/mcp/proxy_controller.py b/server/app/controller/mcp/proxy_controller.py index 8dc978692..790ea6060 100644 --- a/server/app/controller/mcp/proxy_controller.py +++ b/server/app/controller/mcp/proxy_controller.py @@ -111,10 +111,21 @@ def google_search(query: str, search_type: str = "web", key: Key = Depends(key_m def _redact_secret(text: str) -> str: redacted = text for secret in secrets_to_redact: - if secret: + if secret and isinstance(redacted, str): redacted = redacted.replace(secret, "[REDACTED]") return redacted + def _redact_obj(obj): + """Recursively redact secrets from all string fields in a dict/list structure.""" + if isinstance(obj, dict): + return {k: _redact_obj(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [_redact_obj(item) for item in obj] + elif isinstance(obj, str): + return _redact_secret(obj) + else: + return obj + # Using the first page start_page_idx = 1 # Different language may get different result @@ -207,14 +218,10 @@ def _redact_secret(text: str) -> str: logger.info("Google search completed", extra={"query": query, "search_type": search_type, "result_count": len(responses)}) else: error_info = data.get("error", {}) - sanitized_error = { - "code": error_info.get("code"), - "reason": (error_info.get("errors") or [{}])[0].get("reason"), - "message": _redact_secret(error_info.get("message", "")), - } + sanitized_error = _redact_obj(error_info) logger.error( "Google search API error", - extra={"query": query, "search_type": search_type, "api_error": sanitized_error}, + extra={"query": _redact_secret(query), "search_type": _redact_secret(search_type), "api_error": sanitized_error}, ) raise HTTPException(status_code=500, detail="Internal server error") diff --git a/server/app/controller/oauth/oauth_controller.py b/server/app/controller/oauth/oauth_controller.py index 3522d560e..853dcde9d 100644 --- a/server/app/controller/oauth/oauth_controller.py +++ b/server/app/controller/oauth/oauth_controller.py @@ -40,9 +40,14 @@ def oauth_login(app: str, request: Request, state: Optional[str] = None): @traceroot.trace() def oauth_callback(app: str, request: Request, code: Optional[str] = None, state: Optional[str] = None): """Handle OAuth provider callback and redirect to client app.""" - if not code: - logger.warning("OAuth callback missing code", extra={"provider": app}) - raise HTTPException(status_code=400, detail="Missing code parameter") + import re + CODE_STATE_REGEX = re.compile(r'^[A-Za-z0-9_\-]+$') + if not code or not CODE_STATE_REGEX.match(code): + logger.warning("OAuth callback missing or invalid code", extra={"provider": app, "code": code}) + raise HTTPException(status_code=400, detail="Missing or invalid code parameter") + if state and not CODE_STATE_REGEX.match(state): + logger.warning("OAuth callback invalid state", extra={"provider": app, "state": state}) + raise HTTPException(status_code=400, detail="Invalid state parameter") logger.info("OAuth callback received", extra={"provider": app, "has_state": state is not None}) diff --git a/server/app/controller/redirect_controller.py b/server/app/controller/redirect_controller.py index 54121c382..8aee86095 100644 --- a/server/app/controller/redirect_controller.py +++ b/server/app/controller/redirect_controller.py @@ -1,3 +1,4 @@ +import re from urllib.parse import urlencode, quote from fastapi import APIRouter, Request from fastapi.responses import RedirectResponse @@ -8,7 +9,9 @@ @router.get("/redirect/callback") def redirect_callback(code: str, request: Request): - + if not re.match(r'^[A-Za-z0-9_-]+$', code): + # fallback safe redirect without user data + return RedirectResponse("eigent://callback") params = {"code": code} query = urlencode(params, quote_via=quote) redirect_url = f"eigent://callback?{query}" From 2a0dd2fae78619eddde2e91c74175dc4c303aa83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E9=B9=8F=E9=93=96?= <104722516+LuoPengcheng12138@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:14:09 +0800 Subject: [PATCH 7/8] Potential fix for code scanning alert no. 36: Clear-text logging of sensitive information Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- server/app/controller/mcp/proxy_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/app/controller/mcp/proxy_controller.py b/server/app/controller/mcp/proxy_controller.py index 790ea6060..829758e55 100644 --- a/server/app/controller/mcp/proxy_controller.py +++ b/server/app/controller/mcp/proxy_controller.py @@ -229,8 +229,8 @@ def _redact_obj(obj): logger.error( "Google search failed", extra={ - "query": query, - "search_type": search_type, + "query": _redact_secret(query), + "search_type": _redact_secret(search_type), "error_type": type(e).__name__, "error": _redact_secret(str(e)), }, From e2f8a2200fd3d08e16884615a135e5825736a032 Mon Sep 17 00:00:00 2001 From: LuoPengcheng <2653972504@qq.com> Date: Wed, 17 Dec 2025 22:46:01 +0800 Subject: [PATCH 8/8] update security fix --- server/app/controller/mcp/proxy_controller.py | 4 ++-- .../app/controller/oauth/oauth_controller.py | 24 +++++++++++-------- server/app/controller/redirect_controller.py | 20 +++++++++------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/server/app/controller/mcp/proxy_controller.py b/server/app/controller/mcp/proxy_controller.py index 829758e55..5603f4030 100644 --- a/server/app/controller/mcp/proxy_controller.py +++ b/server/app/controller/mcp/proxy_controller.py @@ -215,13 +215,13 @@ def _redact_obj(obj): } responses.append(response) - logger.info("Google search completed", extra={"query": query, "search_type": search_type, "result_count": len(responses)}) + logger.info("Google search completed", extra={"query": _redact_secret(query), "search_type": _redact_secret(search_type), "result_count": len(responses)}) else: error_info = data.get("error", {}) sanitized_error = _redact_obj(error_info) logger.error( "Google search API error", - extra={"query": _redact_secret(query), "search_type": _redact_secret(search_type), "api_error": sanitized_error}, + extra={"query": _redact_secret(query), "search_type": _redact_secret(search_type)}, ) raise HTTPException(status_code=500, detail="Internal server error") diff --git a/server/app/controller/oauth/oauth_controller.py b/server/app/controller/oauth/oauth_controller.py index 853dcde9d..438cef525 100644 --- a/server/app/controller/oauth/oauth_controller.py +++ b/server/app/controller/oauth/oauth_controller.py @@ -35,13 +35,18 @@ def oauth_login(app: str, request: Request, state: Optional[str] = None): logger.error("OAuth login failed", extra={"provider": app, "error": str(e)}, exc_info=True) raise HTTPException(status_code=400, detail="OAuth login failed") - +ALLOWED_OAUTH_PROVIDERS = {"slack", "notion", "x", "googlesuite"} @router.get("/{app}/callback", name="OAuth Callback") @traceroot.trace() def oauth_callback(app: str, request: Request, code: Optional[str] = None, state: Optional[str] = None): """Handle OAuth provider callback and redirect to client app.""" import re CODE_STATE_REGEX = re.compile(r'^[A-Za-z0-9_\-]+$') + from starlette.datastructures import URL + + if app not in ALLOWED_OAUTH_PROVIDERS: + logger.warning("Invalid OAuth provider", extra={"provider": app, "code": code}) + raise HTTPException(status_code=400, detail="Invalid OAuth provider") if not code or not CODE_STATE_REGEX.match(code): logger.warning("OAuth callback missing or invalid code", extra={"provider": app, "code": code}) raise HTTPException(status_code=400, detail="Missing or invalid code parameter") @@ -51,15 +56,14 @@ def oauth_callback(app: str, request: Request, code: Optional[str] = None, state logger.info("OAuth callback received", extra={"provider": app, "has_state": state is not None}) - params = { - "provider": app, - "code": code, - "state": state, - } - query = urlencode(params, quote_via=quote) - - redirect_url = f"eigent://callback/oauth?{query}" - return RedirectResponse(redirect_url) + base_url = URL("eigent://callback/oauth") + redirect_url = base_url.include_query_params( + provider=app, + code=code, + state=state or "", + ) + + return RedirectResponse(str(redirect_url)) @router.post("/{app}/token", name="OAuth Fetch Token") diff --git a/server/app/controller/redirect_controller.py b/server/app/controller/redirect_controller.py index 8aee86095..a90e20e2e 100644 --- a/server/app/controller/redirect_controller.py +++ b/server/app/controller/redirect_controller.py @@ -1,18 +1,20 @@ import re -from urllib.parse import urlencode, quote -from fastapi import APIRouter, Request +from fastapi import APIRouter, Request,HTTPException from fastapi.responses import RedirectResponse - +from utils import traceroot_wrapper as traceroot +logger = traceroot.get_logger("server_redirect_controller") router = APIRouter(tags=["Redirect"]) @router.get("/redirect/callback") def redirect_callback(code: str, request: Request): + from starlette.datastructures import URL + if not re.match(r'^[A-Za-z0-9_-]+$', code): - # fallback safe redirect without user data - return RedirectResponse("eigent://callback") - params = {"code": code} - query = urlencode(params, quote_via=quote) - redirect_url = f"eigent://callback?{query}" - return RedirectResponse(redirect_url) \ No newline at end of file + logger.warning("redirect callback invalid code", extra={"code": code}) + raise HTTPException(status_code=400, detail="Invalid state parameter") + + base_url = URL("eigent://callback") + redirect_url = base_url.include_query_params(code=code) + return RedirectResponse(str(redirect_url)) \ No newline at end of file