mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-17 05:07:23 +08:00
fix: require explicit anonymous webhook access (#14890)
### What problem does this PR solve? Fixes #14882 Agent webhook execution currently fails open when the saved webhook `security` block is missing/empty, or when `auth_type` is set to `none`. This allows unauthenticated webhook invocation without an explicit operator opt-in. This PR makes anonymous webhook access explicit: - Rejects missing or empty webhook security config. - Requires `allow_anonymous: true` when `auth_type` is `none`. - Preserves explicit anonymous webhooks by having the frontend serialize `allow_anonymous: true` when the user selects `None` auth. - Updates webhook unit tests to cover both denied implicit-anonymous configs and allowed explicit-anonymous configs. ### Type of change - [x] Bug Fix - [x] Security hardening - [x] Test ### Tests - [x] `ZHIPU_AI_API_KEY=dummy uv run python -m pytest --confcutdir=test/testcases/test_web_api/test_agent_app test/testcases/test_web_api/test_agent_app/test_agents_webhook_unit.py` - [x] `uv run ruff check api/apps/restful_apis/agent_api.py test/testcases/test_web_api/test_agent_app/test_agents_webhook_unit.py` - [x] `npm exec eslint src/pages/agent/utils.ts src/pages/agent/form/begin-form/schema.ts` --------- Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
This commit is contained in:
@@ -100,6 +100,22 @@ def _require_canvas_owner_sync(func):
|
||||
return wrapper
|
||||
|
||||
|
||||
def _is_truthy(value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, int):
|
||||
return value != 0
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
return False
|
||||
|
||||
|
||||
def _allow_anonymous_webhook(security_cfg: dict) -> bool:
|
||||
if not isinstance(security_cfg, dict):
|
||||
return False
|
||||
return _is_truthy(security_cfg.get("allow_anonymous"))
|
||||
|
||||
|
||||
def _get_user_nickname(user_id: str) -> str:
|
||||
exists, user = UserService.get_by_id(user_id)
|
||||
if not exists:
|
||||
@@ -1567,9 +1583,26 @@ async def agent_chat_completion(tenant_id, agent_id=None):
|
||||
|
||||
|
||||
@manager.route("/agents/<agent_id>/webhook", methods=["POST", "GET", "PUT", "PATCH", "DELETE", "HEAD"]) # noqa: F821
|
||||
@manager.route("/agents/<agent_id>/webhook/test",methods=["POST", "GET", "PUT", "PATCH", "DELETE", "HEAD"],) # noqa: F821
|
||||
async def webhook(agent_id: str):
|
||||
is_test = request.path.startswith(f"/api/v1/agents/{agent_id}/webhook/test")
|
||||
return await _webhook_impl(agent_id, is_test=False)
|
||||
|
||||
|
||||
@manager.route("/agents/<agent_id>/webhook/test", methods=["POST", "GET", "PUT", "PATCH", "DELETE", "HEAD"]) # noqa: F821
|
||||
@login_required
|
||||
@add_tenant_id_to_kwargs
|
||||
async def webhook_test(agent_id: str, tenant_id: str):
|
||||
if not UserCanvasService.query(user_id=tenant_id, id=agent_id):
|
||||
logging.warning(
|
||||
"Webhook test denied: owner check failed agent_id=%s tenant_id=%s method=%s",
|
||||
agent_id,
|
||||
tenant_id,
|
||||
request.method,
|
||||
)
|
||||
return get_json_result(data=False, message="Only the owner of the agent is authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
return await _webhook_impl(agent_id, is_test=True)
|
||||
|
||||
|
||||
async def _webhook_impl(agent_id: str, is_test: bool):
|
||||
start_ts = time.time()
|
||||
|
||||
# 1. Fetch canvas by agent_id
|
||||
@@ -1605,12 +1638,16 @@ async def webhook(agent_id: str):
|
||||
code=RetCode.BAD_REQUEST,message=f"HTTP method '{request_method}' not allowed for this webhook."
|
||||
),RetCode.BAD_REQUEST
|
||||
|
||||
# 6. Validate webhook security
|
||||
async def validate_webhook_security(security_cfg: dict):
|
||||
"""Validate webhook security rules based on security configuration."""
|
||||
|
||||
if not security_cfg:
|
||||
return # No security config → allowed by default
|
||||
if not isinstance(security_cfg, dict) or not security_cfg:
|
||||
logging.warning(
|
||||
"Webhook denied: missing security config agent_id=%s method=%s",
|
||||
agent_id,
|
||||
request.method,
|
||||
)
|
||||
raise Exception("Webhook security is required. Set allow_anonymous to true to permit unauthenticated webhooks.")
|
||||
|
||||
# 1. Validate max body size
|
||||
await _validate_max_body_size(security_cfg)
|
||||
@@ -1625,6 +1662,13 @@ async def webhook(agent_id: str):
|
||||
auth_type = security_cfg.get("auth_type", "none")
|
||||
|
||||
if auth_type == "none":
|
||||
if not _allow_anonymous_webhook(security_cfg):
|
||||
logging.warning(
|
||||
"Webhook denied: anonymous access missing explicit opt-in agent_id=%s method=%s",
|
||||
agent_id,
|
||||
request.method,
|
||||
)
|
||||
raise Exception("Anonymous webhook access requires allow_anonymous to be true")
|
||||
return
|
||||
|
||||
if auth_type == "token":
|
||||
@@ -1643,7 +1687,7 @@ async def webhook(agent_id: str):
|
||||
"""Check request size does not exceed max_body_size."""
|
||||
max_size = security_cfg.get("max_body_size")
|
||||
if not max_size:
|
||||
return
|
||||
max_size = "10MB"
|
||||
|
||||
# Convert "10MB" → bytes
|
||||
units = {"kb": 1024, "mb": 1024**2}
|
||||
@@ -1688,7 +1732,7 @@ async def webhook(agent_id: str):
|
||||
"""Simple in-memory rate limiting."""
|
||||
rl = security_cfg.get("rate_limit")
|
||||
if not rl:
|
||||
return
|
||||
rl = {"limit": 60, "per": "minute"}
|
||||
|
||||
limit = int(rl.get("limit", 60))
|
||||
if limit <= 0:
|
||||
|
||||
Reference in New Issue
Block a user