feat(security): SSRF guard on fetch_url + single-use tool-approval tokens
Two tool/agent-layer hardening changes: * fetch_url now resolves the target host and refuses to connect if any resolved address is loopback, private (RFC1918/ULA), link-local (incl. the 169.254.169.254 cloud-metadata endpoint), multicast, reserved, or unspecified. IPv4-mapped IPv6 is unwrapped first, and the guard re-runs on every redirect hop so a public URL cannot 302 its way to an internal target. * /chat/approve now requires a single-use token minted when the stream pauses for approval and delivered only in that stream's tool_request event, compared in constant time. Previously the pending approval was keyed solely on a client-supplied conversation_id, so anyone who could enumerate a conversation_id could approve another client's pending action. The frontend threads the token from the tool_request event into the approve call. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+15
-6
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json as _json
|
||||
import logging
|
||||
import secrets
|
||||
import threading
|
||||
from typing import AsyncGenerator, Dict, List, Optional, Any
|
||||
|
||||
@@ -171,12 +172,20 @@ async def _run_tool_loop(manager, messages, model, tool_schemas, temperature, nu
|
||||
action_calls = [c for c in calls if _tools.is_action(c.get("function", {}).get("name", ""))]
|
||||
if policy == "ask" and action_calls:
|
||||
event = asyncio.Event()
|
||||
pending_approvals[conversation_id] = {"event": event, "decisions": {}}
|
||||
yield "__approve__" + _json.dumps([
|
||||
{"name": c.get("function", {}).get("name", ""),
|
||||
"arguments": c.get("function", {}).get("arguments")}
|
||||
for c in action_calls
|
||||
])
|
||||
# Single-use capability token, delivered only to the client that owns
|
||||
# this stream. /chat/approve requires it, so knowing the (guessable,
|
||||
# enumerable) conversation_id is no longer enough to approve someone
|
||||
# else's pending action.
|
||||
token = secrets.token_urlsafe(32)
|
||||
pending_approvals[conversation_id] = {"event": event, "decisions": {}, "token": token}
|
||||
yield "__approve__" + _json.dumps({
|
||||
"token": token,
|
||||
"actions": [
|
||||
{"name": c.get("function", {}).get("name", ""),
|
||||
"arguments": c.get("function", {}).get("arguments")}
|
||||
for c in action_calls
|
||||
],
|
||||
})
|
||||
try:
|
||||
await asyncio.wait_for(event.wait(), timeout=_APPROVAL_TIMEOUT)
|
||||
decisions = pending_approvals[conversation_id]["decisions"]
|
||||
|
||||
Reference in New Issue
Block a user