← All news
Prompt InjectionAI AgentsAI Security

A Year Apart, Two Agentforce Leaks Came In Through the Same Public Web Form. Only the Exit Got Fixed

Obiguard Research Team·September 27, 2026·8 min read

On 25 September 2025, Noma Security disclosed ForcedLeak, a CVSS 9.4 flaw in Salesforce Agentforce. An attacker typed hidden instructions into a public Web-to-Lead form. Later, when a salesperson asked Agentforce to look at the new lead, the agent obeyed them and sent CRM data to a domain that was still on Salesforce's allowlist, but had expired. Noma bought it for about $5.

On 25 September 2026, exactly one year later, Zenity Labs disclosed SalesBleed. The attacker again started with the public Web-to-Lead form, and the agent again obeyed text a stranger had typed into it. This time the CRM data went out through a gap in Trusted URLs, the control Salesforce built in response to ForcedLeak.

Both bugs are patched. The question to ask is why the same door let an attacker in twice, and what that means for every other agent built the same way.

What SalesBleed did

Zenity Labs' write-up describes three flaws. Researchers Alex Apostolov, João Donato, Avishai Efrat and Ayush RoyChowdhury reported them to Salesforce on 1 June 2026. The attack chain works like this:

  1. The payload arrives as a sales lead. Web-to-Lead is meant to accept text from anyone on the internet. The attacker submits a lead with instructions hidden in its fields, and it sits in the CRM like any other lead.
  2. An employee asks a routine question. Something like "check my latest leads and help me with the newest one." Agentforce reads the poisoned lead as part of doing its job.
  3. The agent queries data the task didn't need. The injected instructions told the agent's Query Records tool to read the Accounts table. The General CRM subagent already had permission to do that, so nothing stopped it. The proof of concept took company names and deal sizes. Zenity notes the injection "could have asked for anything" that tool can reach.
  4. The data leaves without anyone clicking. The agent put the stolen values into the subdomain of a URL and printed it as an HTML image tag. The interface fetched the image automatically. When the output landed in Slack, link unfurling did the same thing. Either way, the DNS lookup carried the data out before any HTTP-level block could act.

Trusted URLs was supposed to redact any link that wasn't on an approved list. Zenity found two ways around it. The redactor only recognised a fixed set of top-level domains, and .fun wasn't among them. The redactor and the browser also disagreed about where a URL ends: curly braces and square brackets kept a string out of the redactor's view, but the renderer still treated it as a link.

The third flaw turned the agent into a phishing tool. Agentforce's Slack integration could reply to Slack threads without asking the user to confirm. The message appeared as coming from the agent, and the agent kept no record of which user's request caused it. So a poisoned lead could post a convincing internal message into active business threads, and neither the recipient nor the security team could see who was behind it.

The timeline

Date Event
1 June 2026 Zenity reports all three issues to Salesforce
2 June 2026 Salesforce confirms and commits to fixes
18–19 August 2026 Trusted URLs bypass fixed; Zenity verifies
20 August 2026 Slack attribution fix confirmed
21 September 2026 User-confirmation requirement for Slack replies completed
25 September 2026 Public disclosure

No CVEs were assigned. Zenity says Salesforce was collaborative throughout, and the full chain no longer works. It also points out that the new confirmation step for Slack replies can be turned off with a single click. That makes it a setting to check, not just a fix to note.

Same entry, same reach, different exit

Put the two disclosures next to each other and a pattern appears:

ForcedLeak (2025) SalesBleed (2026)
How the instructions got in Public Web-to-Lead form Public Web-to-Lead form
Why the agent could reach the data CRM tools scoped to far more than one lead Query Records could read Accounts during a leads task
How the data left Expired domain still on an allowlist Redaction gaps in Trusted URLs, image auto-load, Slack unfurl
What the fix targeted The exit (Trusted URLs enforcement) The exit (hardened Trusted URLs), plus Slack confirmation and attribution

Both fixes worked, and both were aimed at the exit. That is understandable. Web-to-Lead is supposed to accept text from strangers; you cannot close it. The General CRM subagent is supposed to be broadly useful. The exit is the part that looks fixable.

But an exit filter is a list of known-bad patterns trying to keep up with a model that will write URLs any way it is told. Last year the gap was a forgotten domain. This year it was an unrecognised TLD and a disagreement over a curly brace. Next year there will probably be another. A filter like this is worth having, but on its own it doesn't close the problem.

Researcher Simon Willison has a name for the underlying setup: the "lethal trifecta." An agent that (1) reads untrusted content, (2) can access private data, and (3) can communicate externally can be turned against its owner. Both Agentforce bugs had all three. So does almost every "summarise the inbound request and look up the customer" agent that companies are building right now, whether for support tickets, supplier invoices, job applications or web chat.

We have made a related point before, about email summarisers that read hidden text their users cannot see. SalesBleed adds something worse. The injected text doesn't only change what the agent says. It changes what the agent queries, and which identity it speaks with.

What to do this week

If you run Agentforce:

  • Confirm the confirmation step for Slack replies is enabled, and find out who is able to turn it off.
  • Review what the General CRM subagent's Query Records tool can read. If an agent's job is triaging leads, it shouldn't have read access to Accounts and deal values by default.
  • Search your Web-to-Lead submissions since June for lead fields containing URLs, markup or instruction-like text. The chain was fixed in August, but a stored payload doesn't expire.
  • Treat any Slack message from Agentforce that asks for credentials, approvals or payments with the same suspicion you would give an external sender.

For every agent you build that reads public input:

  • List your trifecta agents. Which agents read text from outside the company and hold data access and produce output that something else will fetch, render or send?
  • Scope tools to the task, not the persona. Lead triage needs the lead, not the accounts table.
  • Treat automatic fetches as outbound traffic. Image rendering, link previews and webhook calls all make requests without a person deciding to.
  • Record who asked. Every action an agent takes should be traceable to the request and identity that caused it.

Where Obiguard fits

Salesforce fixes Agentforce itself. What most organisations lack are the same controls on the other agents they build: agents that read public forms, tickets and email and then call a model. Governance AI sits in the request path for those agents and addresses each part of the trifecta separately, rather than relying on the exit alone.

Inspect what comes in. When an agent pulls a lead, ticket or email into its prompt, that content passes through inspection before it reaches the model. Jailbreak and injection detectors run in-path, and a violation can block the request, flag it, or send it to the Review Queue.

Limit what the agent can reach. Allow-lists register the tools each agent may call. A lead-triage agent approved for one object gets its call to the accounts table blocked, whatever the prompt told it to do. Tool calls are their own decision point in Policy Sets, so "query outside scope" is a rule you write once and attach to the agent.

Check what goes out. Model responses are inspected before delivery, including the URL detector, and the domain allow-list catches attempts to reach unregistered endpoints. This is still an exit control, and it is not the only layer, which is the point.

Know who asked. Every prompt, response, tool call and decision is written to the Audit Ledger with the agent and the initiating identity attached. The anonymous-message problem in SalesBleed's Slack flaw cannot happen when every agent action already carries the user who triggered it.

The question to take away

Salesforce fixed ForcedLeak, then fixed SalesBleed, quickly and cooperatively both times. The pattern is still worth noticing. Two teams of researchers, a year apart, walked in through the same public form, because letting the public in is that form's job.

So the question is not is our exit filter complete? It won't ever be. It is: for each agent that reads text from strangers, what stops that text from changing which data the agent reads, and would you know which user's request set it off?

Explore Governance AI or talk to us about putting inspection, scoped tools and an identity-linked audit trail around the agents that read your public inbox.

How Obiguard helps

Turn this into enforced policy, not just awareness.

Obiguard sits in front of every AI request your organization makes — screening prompts and outputs against the guardrails, compliance frameworks, and audit trails that stories like this one make necessary.

See how it works →