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:
@@ -18,6 +18,7 @@ export function Chatbot({ conversationId, setConversationId, onConversationChang
|
||||
const [images, setImages] = useState([]); // {name, b64} for vision models
|
||||
const [activeTool, setActiveTool] = useState(null); // playbook tool currently running
|
||||
const [pendingApproval, setPendingApproval] = useState(null); // [{name, arguments}] awaiting yes/no
|
||||
const [approvalToken, setApprovalToken] = useState(null); // single-use token authorizing /chat/approve
|
||||
const [editingIdx, setEditingIdx] = useState(null); // user message being edited
|
||||
const [editText, setEditText] = useState("");
|
||||
const [listening, setListening] = useState(false); // mic dictation active
|
||||
@@ -286,7 +287,11 @@ export function Chatbot({ conversationId, setConversationId, onConversationChang
|
||||
continue;
|
||||
}
|
||||
if (pendingEventType === "tool_request") {
|
||||
try { setPendingApproval(JSON.parse(payload)); } catch { /* ignore */ }
|
||||
try {
|
||||
const parsed = JSON.parse(payload);
|
||||
setPendingApproval(parsed.actions || []);
|
||||
setApprovalToken(parsed.token || null);
|
||||
} catch { /* ignore */ }
|
||||
pendingEventType = null;
|
||||
continue;
|
||||
}
|
||||
@@ -401,13 +406,15 @@ export function Chatbot({ conversationId, setConversationId, onConversationChang
|
||||
// Approve or deny the pending action tool(s); the open chat stream resumes.
|
||||
const resolveApproval = async (approve) => {
|
||||
const req = pendingApproval || [];
|
||||
const token = approvalToken;
|
||||
setPendingApproval(null);
|
||||
setApprovalToken(null);
|
||||
const decisions = {};
|
||||
req.forEach(a => { decisions[a.name] = approve; });
|
||||
try {
|
||||
await fetch(`${API_BASE}/chat/approve`, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ conversation_id: conversationId, decisions }),
|
||||
body: JSON.stringify({ conversation_id: conversationId, token, decisions }),
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user