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