mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
fix(a2a): close the taskPushNotificationConfig inline bypass
The A2A 1.0 taskPushNotificationConfig spelling (camelCase or snake_case) survives the 1.0->0.3 conversion into built.configuration.push_notification_ config, but the fallback path returned before the built-object check ran, so an authenticated caller could still register an http:// or loopback webhook via message/send or message/stream. Drop the early return so the built-object validation runs on both paths, and extend the raw wire check with the taskPushNotificationConfig / task_push_notification_config spellings for the configs the conversion does drop. Reviewed-by: veria-ai[bot] (flagged at a2a_endpoints.py:166)
This commit is contained in:
parent
51d36e86cd
commit
324b28dddd
2 changed files with 73 additions and 7 deletions
|
|
@ -161,12 +161,13 @@ def _build_message_send_params(params: dict[str, Any]) -> "MessageSendParams":
|
|||
raise ValueError(f"Invalid message/send params: {e}") from e
|
||||
built = to_compat_send_message_request(pb, "").params
|
||||
# The 1.0->0.3 conversion drops pushNotificationConfig, so validate the
|
||||
# raw wire config on this path.
|
||||
# raw wire config on this path too.
|
||||
_validate_inline_push_notification_url(params)
|
||||
return built
|
||||
# A push config registered inline via message/send or message/stream must
|
||||
# clear the same SSRF guard the tasks/pushNotificationConfig/set method
|
||||
# applies, otherwise message/send is a way around it (#49622).
|
||||
# applies, otherwise message/send is a way around it (#49622). Runs on both
|
||||
# paths: the 1.0 taskPushNotificationConfig spelling survives the
|
||||
# conversion into built.configuration.push_notification_config.
|
||||
push_config = built.configuration.push_notification_config if built.configuration else None
|
||||
callback_url = push_config.url if push_config is not None else None
|
||||
if callback_url:
|
||||
|
|
@ -177,14 +178,26 @@ def _build_message_send_params(params: dict[str, Any]) -> "MessageSendParams":
|
|||
def _validate_inline_push_notification_url(params: Mapping[str, Any]) -> None:
|
||||
"""Reject an unsafe inline pushNotificationConfig carried in wire params.
|
||||
|
||||
Covers the A2A 1.0 shape (``configuration.pushNotificationConfig.url``) on the
|
||||
proto-fallback path, where the built params no longer carry the config.
|
||||
Covers the A2A 1.0 shapes (``configuration.pushNotificationConfig.url`` and
|
||||
``configuration.taskPushNotificationConfig.url``, camelCase or snake_case)
|
||||
on the proto-fallback path, where a dropped config no longer exists in the
|
||||
built params.
|
||||
"""
|
||||
configuration = params.get("configuration")
|
||||
if not isinstance(configuration, Mapping):
|
||||
return
|
||||
push_config = configuration.get("pushNotificationConfig") or configuration.get("push_notification_config")
|
||||
if not isinstance(push_config, Mapping):
|
||||
push_config: Any = None
|
||||
for key in (
|
||||
"pushNotificationConfig",
|
||||
"push_notification_config",
|
||||
"taskPushNotificationConfig",
|
||||
"task_push_notification_config",
|
||||
):
|
||||
value = configuration.get(key)
|
||||
if isinstance(value, Mapping):
|
||||
push_config = value
|
||||
break
|
||||
if push_config is None:
|
||||
return
|
||||
callback_url = push_config.get("url")
|
||||
if isinstance(callback_url, str) and callback_url:
|
||||
|
|
|
|||
|
|
@ -3296,3 +3296,56 @@ def test_message_send_params_without_push_config_is_untouched():
|
|||
}
|
||||
)
|
||||
assert result.message.role.value == "user"
|
||||
|
||||
|
||||
def test_message_send_params_rejects_a2a_10_task_push_url_loopback():
|
||||
"""Regression (#49637 review): A2A 1.0 taskPushNotificationConfig survives the
|
||||
1.0->0.3 conversion into built.configuration.push_notification_config, so the
|
||||
built-object check must run on the fallback path too — a loopback webhook on
|
||||
message/send is refused before reaching the agent."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm.proxy.agent_endpoints.a2a_endpoints import _build_message_send_params
|
||||
|
||||
params = {
|
||||
"message": {
|
||||
"messageId": "msg-1",
|
||||
"role": "ROLE_USER",
|
||||
"parts": [{"text": "hello"}],
|
||||
},
|
||||
"configuration": {
|
||||
"taskPushNotificationConfig": {
|
||||
"url": "http://127.0.0.1:8080/hook",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_build_message_send_params(params)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
|
||||
def test_message_send_params_rejects_snake_case_task_push_url():
|
||||
"""The snake_case task_push_notification_config wire spelling is covered too."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm.proxy.agent_endpoints.a2a_endpoints import _build_message_send_params
|
||||
|
||||
params = {
|
||||
"message": {
|
||||
"messageId": "msg-1",
|
||||
"role": "ROLE_USER",
|
||||
"parts": [{"text": "hello"}],
|
||||
},
|
||||
"configuration": {
|
||||
"task_push_notification_config": {
|
||||
"url": "http://10.0.0.9/hook",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_build_message_send_params(params)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue