CVE-2026-1721
Description
Summary
A Reflected Cross-Site Scripting (XSS) vulnerability was discovered in the AI Playground's OAuth callback handler. The error_description query parameter was directly interpolated into an HTML script tag without proper escaping, allowing attackers to execute arbitrary JavaScript in the context of the victim's session.
Root cause
The OAuth callback handler in site/ai-playground/src/server.ts directly interpolated the authError value, sourced from the error_description query parameter, into an inline <script> tag.
Impact
An attacker could craft a malicious link that, when clicked by a victim, would:
- Steal user chat message history - Access all LLM interactions stored in the user's session.
- Access connected MCP Servers - Interact with any MCP servers connected to the victim's session (public or authenticated/private), potentially allowing the attacker to perform actions on the victim's behalf
Mitigation:
- PR: https://github.com/cloudflare/agents/pull/841 https://github.com/cloudflare/agents/pull/841
- Agents-sdk users should upgrade to agents@0.3.10
- Developers using configureOAuthCallback with custom error handling in their own applications should ensure all user-controlled input is escaped before interpolation.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
agentsnpm | < 0.3.10 | 0.3.10 |
Affected products
1Patches
13f490d045844fix: escape html in external oauth error message (#841)
12 files changed · +224 −143
.changeset/rare-windows-bathe.md+5 −0 added@@ -0,0 +1,5 @@ +--- +"agents": patch +--- + +Escape authError to prevent XSS attacks and store it in the connection state to avoid needing script tags to display error.
examples/mcp-client/src/client.tsx+1 −0 modified@@ -69,6 +69,7 @@ function App() { placeholder: { auth_url: null, capabilities: null, + error: null, instructions: null, name: serverName, server_url: serverUrl,
examples/mcp-client/src/server.ts+5 −17 modified@@ -1,26 +1,14 @@ import { Agent, routeAgentRequest } from "agents"; -import type { MCPClientOAuthResult } from "agents/mcp"; export class MyAgent extends Agent { onStart() { // Optionally configure OAuth callback. Here we use popup-closing behavior since we're opening a window on the client this.mcp.configureOAuthCallback({ - customHandler: (result: MCPClientOAuthResult) => { - if (result.authSuccess) { - return new Response("<script>window.close();</script>", { - headers: { "content-type": "text/html" }, - status: 200 - }); - } else { - const safeError = JSON.stringify(result.authError || "Unknown error"); - return new Response( - `<script>alert('Authentication failed: ' + ${safeError}); window.close();</script>`, - { - headers: { "content-type": "text/html" }, - status: 200 - } - ); - } + customHandler: () => { + return new Response("<script>window.close();</script>", { + headers: { "content-type": "text/html" }, + status: 200 + }); } }); }
package-lock.json+9 −0 modified@@ -11006,6 +11006,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/escape-html": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", + "integrity": "sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -27460,6 +27467,7 @@ "@cfworker/json-schema": "^4.1.1", "@modelcontextprotocol/sdk": "1.25.2", "cron-schedule": "^6.0.0", + "escape-html": "^1.0.3", "json-schema": "^0.4.0", "json-schema-to-typescript": "^15.0.4", "mimetext": "^3.0.28", @@ -27475,6 +27483,7 @@ "@ai-sdk/openai": "^3.0.23", "@ai-sdk/react": "^3.0.66", "@cloudflare/workers-oauth-provider": "^0.2.2", + "@types/escape-html": "^1.0.4", "@types/react": "^19.2.10", "@types/yargs": "^17.0.35", "ai": "^6.0.64",
packages/agents/package.json+2 −0 modified@@ -27,6 +27,7 @@ "@cfworker/json-schema": "^4.1.1", "@modelcontextprotocol/sdk": "1.25.2", "cron-schedule": "^6.0.0", + "escape-html": "^1.0.3", "json-schema": "^0.4.0", "json-schema-to-typescript": "^15.0.4", "mimetext": "^3.0.28", @@ -39,6 +40,7 @@ "@ai-sdk/openai": "^3.0.23", "@ai-sdk/react": "^3.0.66", "@cloudflare/workers-oauth-provider": "^0.2.2", + "@types/escape-html": "^1.0.4", "@types/react": "^19.2.10", "@types/yargs": "^17.0.35", "ai": "^6.0.64",
packages/agents/src/index.ts+2 −0 modified@@ -259,6 +259,7 @@ export type MCPServer = { // Scope outside of that can't be relied upon because when the DO sleeps, there's no way // to communicate a change to a non-ready state. state: MCPConnectionState; + error: string | null; instructions: string | null; capabilities: ServerCapabilities | null; }; @@ -3182,6 +3183,7 @@ export class Agent< mcpState.servers[server.id] = { auth_url: server.auth_url, capabilities: serverConn?.serverCapabilities ?? null, + error: serverConn?.connectionError ?? null, instructions: serverConn?.instructions ?? null, name: server.name, server_url: server.server_url,
packages/agents/src/mcp/client-connection.ts+1 −0 modified@@ -95,6 +95,7 @@ export type MCPDiscoveryResult = { export class MCPClientConnection { client: Client; connectionState: MCPConnectionState = MCPConnectionState.CONNECTING; + connectionError: string | null = null; lastConnectedTransport: BaseTransportType | undefined; instructions?: string; tools: Tool[] = [];
packages/agents/src/mcp/client.ts+63 −71 modified@@ -1,4 +1,5 @@ import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import escapeHtml from "escape-html"; import type { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js"; import type { CallToolRequest, @@ -44,6 +45,13 @@ export type MCPServerOptions = { }; }; +/** + * Result of an OAuth callback request + */ +export type MCPOAuthCallbackResult = + | { serverId: string; authSuccess: true; authError?: undefined } + | { serverId: string; authSuccess: false; authError: string }; + /** * Options for registering an MCP server */ @@ -187,6 +195,19 @@ export class MCPClientManager { ); } + private failConnection( + serverId: string, + error: string + ): MCPOAuthCallbackResult { + this.clearServerAuthUrl(serverId); + if (this.mcpConnections[serverId]) { + this.mcpConnections[serverId].connectionState = MCPConnectionState.FAILED; + this.mcpConnections[serverId].connectionError = error; + } + this._onServerStateChanged.fire(); + return { serverId, authSuccess: false, authError: error }; + } + jsonSchema: typeof import("ai").jsonSchema | undefined; /** @@ -663,19 +684,19 @@ export class MCPClientManager { return servers.some((server) => server.id === serverId); } - async handleCallbackRequest(req: Request) { + async handleCallbackRequest(req: Request): Promise<MCPOAuthCallbackResult> { const url = new URL(req.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); const error = url.searchParams.get("error"); const errorDescription = url.searchParams.get("error_description"); + // Early validation - these throw because we can't identify the connection if (!state) { throw new Error("Unauthorized: no state provided"); } const serverId = this.extractServerIdFromState(state); - if (!serverId) { throw new Error( "No serverId found in state parameter. Expected format: {nonce}.{serverId}" @@ -684,7 +705,6 @@ export class MCPClientManager { const servers = this.getServersFromStorage(); const serverExists = servers.some((server) => server.id === serverId); - if (!serverExists) { throw new Error( `No server found with id "${serverId}". Was the request matched with \`isCallbackRequest()\`?` @@ -695,89 +715,61 @@ export class MCPClientManager { throw new Error(`Could not find serverId: ${serverId}`); } + // We have a valid connection - all errors from here should fail the connection const conn = this.mcpConnections[serverId]; - if (!conn.options.transport.authProvider) { - throw new Error( - "Trying to finalize authentication for a server connection without an authProvider" - ); - } - const authProvider = conn.options.transport.authProvider; - authProvider.serverId = serverId; + try { + if (!conn.options.transport.authProvider) { + throw new Error( + "Trying to finalize authentication for a server connection without an authProvider" + ); + } - // Two-phase state validation: check first (non-destructive), consume later - // This prevents DoS attacks where attacker consumes valid state before legitimate callback - const stateValidation = await authProvider.checkState(state); - if (!stateValidation.valid) { - this.clearServerAuthUrl(serverId); - if (this.mcpConnections[serverId]) { - this.mcpConnections[serverId].connectionState = - MCPConnectionState.FAILED; + const authProvider = conn.options.transport.authProvider; + authProvider.serverId = serverId; + + // Two-phase state validation: check first (non-destructive), consume later + // This prevents DoS attacks where attacker consumes valid state before legitimate callback + const stateValidation = await authProvider.checkState(state); + if (!stateValidation.valid) { + throw new Error(stateValidation.error || "Invalid state"); } - this._onServerStateChanged.fire(); - return { - serverId, - authSuccess: false, - authError: stateValidation.error || "Invalid state" - }; - } - if (error) { - return { - serverId, - authSuccess: false, - authError: errorDescription || error - }; - } + if (error) { + // Escape external OAuth error params to prevent XSS + throw new Error(escapeHtml(errorDescription || error)); + } - if (!code) { - throw new Error("Unauthorized: no code provided"); - } + if (!code) { + throw new Error("Unauthorized: no code provided"); + } - if ( - this.mcpConnections[serverId].connectionState === - MCPConnectionState.READY || - this.mcpConnections[serverId].connectionState === - MCPConnectionState.CONNECTED - ) { - this.clearServerAuthUrl(serverId); - return { - serverId, - authSuccess: true - }; - } + // Already authenticated - just return success + if ( + conn.connectionState === MCPConnectionState.READY || + conn.connectionState === MCPConnectionState.CONNECTED + ) { + this.clearServerAuthUrl(serverId); + return { serverId, authSuccess: true }; + } - if ( - this.mcpConnections[serverId].connectionState !== - MCPConnectionState.AUTHENTICATING - ) { - throw new Error( - `Failed to authenticate: the client is in "${this.mcpConnections[serverId].connectionState}" state, expected "authenticating"` - ); - } + if (conn.connectionState !== MCPConnectionState.AUTHENTICATING) { + throw new Error( + `Failed to authenticate: the client is in "${conn.connectionState}" state, expected "authenticating"` + ); + } - try { await authProvider.consumeState(state); await conn.completeAuthorization(code); await authProvider.deleteCodeVerifier(); this.clearServerAuthUrl(serverId); + conn.connectionError = null; this._onServerStateChanged.fire(); - return { - serverId, - authSuccess: true - }; - } catch (authError) { - const errorMessage = - authError instanceof Error ? authError.message : String(authError); - - this._onServerStateChanged.fire(); - - return { - serverId, - authSuccess: false, - authError: errorMessage - }; + return { serverId, authSuccess: true }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return this.failConnection(serverId, message); } }
packages/agents/src/tests/mcp/client-manager.test.ts+101 −8 modified@@ -399,7 +399,7 @@ describe("MCPClientManager OAuth Integration", () => { expect(result.authError).toBe("User denied access"); }); - it("should throw error for callback without code or error", async () => { + it("should fail connection for callback without code or error", async () => { const serverId = "server1"; const callbackUrl = "http://localhost:3000/callback"; const stateStorage = createMockStateStorage(); @@ -429,9 +429,9 @@ describe("MCPClientManager OAuth Integration", () => { const state = stateStorage.createState(serverId); const callbackRequest = new Request(`${callbackUrl}?state=${state}`); - await expect( - manager.handleCallbackRequest(callbackRequest) - ).rejects.toThrow("Unauthorized: no code provided"); + const result = await manager.handleCallbackRequest(callbackRequest); + expect(result.authSuccess).toBe(false); + expect(result.authError).toBe("Unauthorized: no code provided"); }); it("should throw error for callback without state", async () => { @@ -496,7 +496,7 @@ describe("MCPClientManager OAuth Integration", () => { expect(result.serverId).toBe(serverId); }); - it("should error when callback received for connection in failed state", async () => { + it("should fail connection when callback received for connection in failed state", async () => { const serverId = "test-server"; const callbackUrl = "http://localhost:3000/callback"; const stateStorage = createMockStateStorage(); @@ -532,9 +532,9 @@ describe("MCPClientManager OAuth Integration", () => { `${callbackUrl}?code=test&state=${state}` ); - await expect( - manager.handleCallbackRequest(callbackRequest) - ).rejects.toThrow( + const result = await manager.handleCallbackRequest(callbackRequest); + expect(result.authSuccess).toBe(false); + expect(result.authError).toBe( 'Failed to authenticate: the client is in "failed" state, expected "authenticating"' ); }); @@ -813,6 +813,9 @@ describe("MCPClientManager OAuth Integration", () => { expect(result.authSuccess).toBe(false); expect(result.serverId).toBe(serverId); expect(result.authError).toBe("User denied access"); + // Verify error is stored on connection for UI display + expect(connection.connectionState).toBe("failed"); + expect(connection.connectionError).toBe("User denied access"); }); it("should handle OAuth error without description", async () => { @@ -853,6 +856,96 @@ describe("MCPClientManager OAuth Integration", () => { expect(result.authError).toBe("server_error"); }); + it("should escape XSS payloads in error_description", async () => { + const serverId = "test-server"; + const callbackUrl = "http://localhost:3000/callback"; + const stateStorage = createMockStateStorage(); + + saveServerToMock({ + id: serverId, + name: "Test Server", + server_url: "http://test.com", + callback_url: callbackUrl, + client_id: "test-client-id", + auth_url: null, + server_options: null + }); + + const mockAuthProvider = createMockAuthProvider(stateStorage); + const connection = new MCPClientConnection( + new URL("http://test.com"), + { name: "test-client", version: "1.0.0" }, + { + transport: { type: "auto", authProvider: mockAuthProvider }, + client: {} + } + ); + connection.connectionState = "authenticating"; + manager.mcpConnections[serverId] = connection; + + const state = stateStorage.createState(serverId); + const xssPayload = "</script><img src=x onerror=alert(1)>"; + const callbackRequest = new Request( + `${callbackUrl}?error=access_denied&error_description=${encodeURIComponent(xssPayload)}&state=${state}` + ); + const result = await manager.handleCallbackRequest(callbackRequest); + + expect(result.authSuccess).toBe(false); + // Verify XSS payload is escaped + expect(result.authError).toBe( + "</script><img src=x onerror=alert(1)>" + ); + expect(connection.connectionError).toBe( + "</script><img src=x onerror=alert(1)>" + ); + // Should not contain raw script tag + expect(result.authError).not.toContain("<script>"); + expect(result.authError).not.toContain("</script>"); + }); + + it("should escape XSS payloads in error parameter when description is absent", async () => { + const serverId = "test-server"; + const callbackUrl = "http://localhost:3000/callback"; + const stateStorage = createMockStateStorage(); + + saveServerToMock({ + id: serverId, + name: "Test Server", + server_url: "http://test.com", + callback_url: callbackUrl, + client_id: "test-client-id", + auth_url: null, + server_options: null + }); + + const mockAuthProvider = createMockAuthProvider(stateStorage); + const connection = new MCPClientConnection( + new URL("http://test.com"), + { name: "test-client", version: "1.0.0" }, + { + transport: { type: "auto", authProvider: mockAuthProvider }, + client: {} + } + ); + connection.connectionState = "authenticating"; + manager.mcpConnections[serverId] = connection; + + const state = stateStorage.createState(serverId); + const xssPayload = "<script>alert('xss')</script>"; + const callbackRequest = new Request( + `${callbackUrl}?error=${encodeURIComponent(xssPayload)}&state=${state}` + ); + const result = await manager.handleCallbackRequest(callbackRequest); + + expect(result.authSuccess).toBe(false); + expect(result.authError).toBe( + "<script>alert('xss')</script>" + ); + expect(connection.connectionError).toBe( + "<script>alert('xss')</script>" + ); + }); + it("should handle token exchange failure", async () => { const serverId = "test-server"; const callbackUrl = "http://localhost:3000/callback";
site/ai-playground/src/app.tsx+2 −1 modified@@ -110,7 +110,8 @@ const App = () => { id, name: server.name, url: server.server_url, - state: server.state + state: server.state, + error: server.error }) );
site/ai-playground/src/components/McpServers.tsx+28 −29 modified@@ -12,6 +12,7 @@ export type McpServerInfo = { name?: string; url?: string; state: string; + error?: string | null; }; export type McpServersComponentState = { @@ -61,14 +62,6 @@ export function McpServers({ agent, mcpState, mcpLogs }: McpServersProps) { (s) => s.state === "authenticating" ); - // Clear error when a server becomes ready - useEffect(() => { - const hasReadyServer = mcpState.servers.some((s) => s.state === "ready"); - if (hasReadyServer) { - setError(""); - } - }, [mcpState.servers]); - const logRef = useRef<HTMLDivElement>(null); const [showAuth, setShowAuth] = useState<boolean>(false); const [headerKey, setHeaderKey] = useState<string>(() => { @@ -364,11 +357,6 @@ export function McpServers({ agent, mcpState, mcpLogs }: McpServersProps) { </p> <div className="my-4"> - {/* Error display */} - {error && ( - <div className="mb-2 text-xs text-red-600 truncate">{error}</div> - )} - {/* Add new server form - URL input + key icon + Add button */} <div className="relative mb-4"> <div className="flex space-x-2"> @@ -515,25 +503,36 @@ export function McpServers({ agent, mcpState, mcpLogs }: McpServersProps) { {mcpState.servers.map((server) => ( <div key={server.id} - className="flex items-center justify-between p-2 border border-gray-200 rounded-md bg-gray-50" + className={`p-2 border rounded-md ${ + server.state === "failed" + ? "border-red-200 bg-red-50" + : "border-gray-200 bg-gray-50" + }`} > - <div className="flex items-center space-x-2 min-w-0 flex-1"> - {getStatusBadge(server.state)} - <span - className="text-sm text-gray-700 truncate" - title={server.url} + <div className="flex items-center justify-between"> + <div className="flex items-center space-x-2 min-w-0 flex-1"> + {getStatusBadge(server.state)} + <span + className="text-sm text-gray-700 truncate" + title={server.url} + > + {server.url} + </span> + </div> + <button + type="button" + className="ml-2 px-2 py-1 text-xs bg-red-100 hover:bg-red-200 text-red-700 rounded-md transition-colors shrink-0" + onClick={() => handleDisconnect(server.id)} + disabled={disconnectingServerId === server.id} > - {server.url} - </span> + {disconnectingServerId === server.id ? "..." : "×"} + </button> </div> - <button - type="button" - className="ml-2 px-2 py-1 text-xs bg-red-100 hover:bg-red-200 text-red-700 rounded-md transition-colors shrink-0" - onClick={() => handleDisconnect(server.id)} - disabled={disconnectingServerId === server.id} - > - {disconnectingServerId === server.id ? "..." : "×"} - </button> + {server.state === "failed" && server.error && ( + <div className="mt-2 text-xs text-red-600 break-words"> + {server.error} + </div> + )} </div> ))} </div>
site/ai-playground/src/server.ts+5 −17 modified@@ -1,7 +1,6 @@ import { createWorkersAI } from "workers-ai-provider"; import { callable, routeAgentRequest } from "agents"; import { AIChatAgent } from "@cloudflare/ai-chat"; -import type { MCPClientOAuthResult } from "agents/mcp"; import { convertToModelMessages, createUIMessageStream, @@ -66,23 +65,12 @@ export class Playground extends AIChatAgent<Env, PlaygroundState> { }; onStart() { - // Configure OAuth callback to close popup window after authentication this.mcp.configureOAuthCallback({ - customHandler: (result: MCPClientOAuthResult) => { - if (result.authSuccess) { - return new Response("<script>window.close();</script>", { - headers: { "content-type": "text/html" }, - status: 200 - }); - } - const safeError = JSON.stringify(result.authError || "Unknown error"); - return new Response( - `<script>alert('Authentication failed: ' + ${safeError}); window.close();</script>`, - { - headers: { "content-type": "text/html" }, - status: 200 - } - ); + customHandler: () => { + return new Response("<script>window.close();</script>", { + headers: { "content-type": "text/html" }, + status: 200 + }); } }); }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
50- The hidden risk of non-human identities in AI adoptionHelp Net Security · May 13, 2026
- SAP unveils Autonomous Enterprise for AI-driven business operationsHelp Net Security · May 12, 2026
- ThreatDown ITDR prevents credential-based attacksHelp Net Security · May 12, 2026
- Amazon Quick authorization bypass let users reach blocked AI chat agentsHelp Net Security · May 12, 2026
- How Rapid7 is bringing Cyber GRC closer to security operationsRapid7 Blog · May 12, 2026
- Alation AI Governance creates a system of record for AI oversightHelp Net Security · May 11, 2026
- SailPoint Agentic Fabric expands identity governance to autonomous AI agentsHelp Net Security · May 11, 2026
- The questionnaire-based TPRM model is broken, and TrustCloud has a fixHelp Net Security · May 11, 2026
- Arctic Wolf kicks 250 employees out of the pack to save money for AIThe Register Security · May 6, 2026
- Sysdig delivers cloud security that runs inside AI coding agentsHelp Net Security · May 6, 2026
- Your AI Agents Are Already Inside the Perimeter. Do You Know What They're Doing?The Hacker News · May 6, 2026
- Extreme Networks introduces Agent ONE for autonomous enterprise networkingHelp Net Security · May 6, 2026
- 8×8 updates CX platform with AI, analytics, and frontline management capabilitiesHelp Net Security · May 6, 2026
- New Relic advances AI observability with new intelligence layerHelp Net Security · May 6, 2026
- Risky Business #836 -- You can't patch the bugpocalypseRisky Business · May 6, 2026
- ServiceNow clears agents for landing with new AI control towerThe Register Security · May 5, 2026
- One in four MCP servers opens AI agent security to code execution riskHelp Net Security · May 5, 2026
- Cisco Moves to Acquire Astrix Security to Tackle Non-Human Identity RisksSecurityWeek · May 4, 2026
- TeamPCP Weekly Analysis: 2026-W18 (2026-04-27 through 2026-05-03), (Mon, May 4th)SANS Internet Storm Center · May 4, 2026
- Shadow IT has given way to shadow AI. Enter AI-BOMsThe Register Security · May 4, 2026
- Shadow IT has given way to shadow AI. Enter AI-BOMsThe Register Security · May 4, 2026
- 4th May – Threat Intelligence ReportCheck Point Research · May 4, 2026
- Operant AI Endpoint Protector secures AI agents and MCP toolsHelp Net Security · May 4, 2026
- Security for AI: A strategic framework for closing the AI exposure gapTenable Blog · May 4, 2026
- Blend Autopilot MCP brings AI agent orchestration to lending platformsHelp Net Security · May 4, 2026
- Lens Agents brings policy control to AI across cloud and desktopHelp Net Security · May 4, 2026
- Pipelock: Open-source AI agent firewallHelp Net Security · May 4, 2026
- Five Eyes spook shops warn rapid rollouts of agentic AI are too riskyThe Register Security · May 4, 2026
- Week in review: High-severity LPE vulnerability in the Linux kernel, cPanel 0-day exploited for monthsHelp Net Security · May 3, 2026
- Code Orange: Fail Small is complete. The result is a stronger Cloudflare networkCloudflare Blog · May 1, 2026
- Metasploit Wrap-Up 05/01/2026Rapid7 Blog · May 1, 2026
- The Good, the Bad and the Ugly in Cybersecurity – Week 18SentinelOne Labs · May 1, 2026
- Vulnerability remediation: Match CVEs to asset owners in seconds with Tenable Hexa AITenable Blog · May 1, 2026
- Introducing Dynamic Workflows: durable execution that follows the tenantCloudflare Blog · May 1, 2026
- Great responsibility, without great powerCisco Talos Intelligence · Apr 30, 2026
- Agents can now create Cloudflare accounts, buy domains, and deployCloudflare Blog · Apr 30, 2026
- Yet another experiment proves it's too damn simple to poison large language modelsThe Register Security · Apr 29, 2026
- Mastering agentic AI security through exposure managementTenable Blog · Apr 29, 2026
- Webinar: How to Automate Exposure Validation to Match the Speed of AI AttacksThe Hacker News · Apr 29, 2026
- AI-powered honeypots: Turning the tables on malicious AI agentsCisco Talos Intelligence · Apr 29, 2026
- 30 ClawHub skills secretly turn AI agents into a crypto swarmThe Register Security · Apr 29, 2026
- 30 ClawHub skills secretly turn AI agents into a crypto swarmThe Register Security · Apr 29, 2026
- What Anthropic’s Mythos Means for the Future of CybersecuritySchneier on Security · Apr 28, 2026
- Microsoft Patches Entra ID Role Flaw That Enabled Service Principal TakeoverThe Hacker News · Apr 28, 2026
- Cursor-Opus agent snuffs out startup’s production databaseThe Register Security · Apr 27, 2026
- Glasswing Secured the Code. The Rest of Your Stack Is Still on YouDark Reading · Apr 24, 2026
- Bridging the AI Agent Authority Gap: Continuous Observability as the Decision EngineThe Hacker News · Apr 24, 2026
- Five steps to become Mythos readyTenable Blog · Apr 23, 2026
- Bad Memories Still Haunt AI AgentsDark Reading · Apr 23, 2026
- AI is Changing Vulnerability Discovery and your Software Supply Chain Strategy has to Change with itRapid7 Blog · Apr 23, 2026