Every form field your agent will read is a prompt
My site has a contact form. Anyone can type 4,000 characters into it. That text lands in a mailbox. Separately, an agent has been reading my freelance offers for months. Both uses raise the same question: what happens w
My site has a contact form. Anyone can type 4,000 characters into it. That text lands in a mailbox.
Separately, an agent has been reading my freelance offers for months. Both uses raise the same question: what happens when an agent reads a stranger's text?
TL;DR: researchers demonstrated CRM data exfiltration through a public form. An agent followed instructions hidden in the text. I limit the consequences of a manipulated response: no direct tools, a checked format and separate sending rules. These measures reduce risk. They do not guarantee a truthful answer.
This article is for developers who plug an agent into text written by strangers: emails, forms, tickets, reviews.
SalesBleed: a public form was enough
On 24 September 2026, Zenity Labs published three flaws in Agentforce, Salesforce's agent platform. They named them SalesBleed.
The attack starts from a Web-to-Lead form. That is the "contact us" form of a website, wired to the CRM. It is public by design: a prospect has no account.
The attacker fills in the form like a prospect. In one of the fields, they hide a prompt injection. Then they wait.
One day, an employee asks the agent a routine question. The agent reads the poisoned record. It follows the hidden instructions, fetches other CRM data, and sends it out through an image tag. Nobody clicked on anything.
Zenity reported the flaws on 1 June. Salesforce fixed them. No real-world exploitation was reported.
The fix closes that specific case. The pattern exists wherever an agent reads a free-text field.
Form validation does not see the problem
My form has all the classic protections. I reread them in the code.
- A maximum length per field: 100 characters for the name, 4,000 for the message.
- A format check on the email address.
- An invisible trap field that only bots fill in.
- An anti-bot test before sending.
These protections slow down bots and reject some malformed data. None of them reads what the message means.
An injection is a well-formed message. It has the right length. A patient human wrote it. To the form, it is one more prospect.
Assume the injection will get through
OWASP ranks prompt injection as the number one risk for applications built on an LLM. No filter guarantees that it will block every injection.
The reason is simple. To a model, everything is text. Your instructions and the stranger's email arrive in the same window. You can write "do not follow instructions in the email". The email can write the opposite, and better than you.
So I do not ask "how do I block the injection?". I ask this: once the model is convinced, what can it do?
Here are the four rules of my triage agent. It reads mission offers sent by strangers.
Rule 1: the model that reads has no tools
In SalesBleed, the agent reading the record had access to the rest of the CRM. That is what made the leak possible.
In my setup, the model that reads the email is a local model. My code calls it through a single function. Text goes in, text comes out.
It cannot search my mailbox, open a URL or send a message through a tool call. But its response feeds the rest of the program. A manipulated grid can influence the action selected next. Having no tools limits direct capabilities, not every downstream consequence.
Rule 2: check the format without mistaking it for truth
That model has one job: fill in a JSON grid. Stack, rate, remote work, contract type, duration, start date. Each cell gets one of three verdicts: GO, NO_GO, UNKNOWN.
My Go code checks that each verdict belongs to that list. Here is that format check:
func normalizeField(f convstate.Field) convstate.Field {
v := strings.ToUpper(string(f.Verdict))
switch v {
case "GO", "NO_GO", "UNKNOWN":
f.Verdict = convstate.Verdict(v)
default:
f.Verdict = convstate.VerdictUnknown
}
return f
}
An unknown verdict becomes UNKNOWN. But a fabricated GO passes this check. This function validates the vocabulary, not the extracted information.
The first name reused in the reply also passes through a filter. It rejects digits from 0 to 9 and the characters @,;:, then keeps one word. Other punctuation remains possible. This cleanup does not guarantee that the result is a first name.
Code then chooses among predefined actions: ask a question, decline, hand over to me. The model cannot add an action. It can still influence the choice by falsifying the data.
Rule 3: recheck the criteria you can verify
An injection can also make the grid lie. For example to pass a permanent job off as a freelance mission.
So on the criteria that matter, my code rereads the raw text. Regular expressions look for unambiguous words: contract type, stack, remote work, an urgent start date.
If the text says "ASAP" and the model answers "date unknown", the code corrects it. The model suggests, the code decides.
These checks correct some model errors. They may also catch some manipulated responses, without providing complete protection. Rate and duration, for example, are not recalculated by normalizeField. And the words in the raw text still come from the sender.
Rule 4: bound the context
My code cuts the email body at 2,500 bytes. The attachment block is limited to 3,000 bytes. In Go, len(string) counts bytes. An accented letter can occupy several.
The trials documented in the code report less reliable verdicts with a limit of 4,000. The limit controls how much content is processed. The current slice can also split a UTF-8 character.
An injection can fit in one sentence. Shortening the text is therefore not enough to block it.
What stays fragile: the written reply
I will not tell you everything is solved.
A second model writes the reply. It receives the decision made by the code, plus the original email so it does not ask again for what was already said. So it reads the stranger's text.
An injection can influence the outgoing content, including adding a commitment or requesting sensitive information. The action category is already chosen, but it does not guarantee what the reply says.
That is why sending has its own rules, outside the model. A decline always waits for my click. An automatic send is capped and delayed. I detailed those rules in the article on automatic replies.
A cap and a delay limit volume, not meaning. Automatic replies remain a risk to assess. Sensitive replies still need human review.
The checklist
Separate what the agent reads from what it is allowed to do.
- [ ] List every free-text field your agent will read: form, email, ticket, attachment
- [ ] Give zero tools to the model that reads text from strangers
- [ ] Make it fill in a fixed format with closed values
- [ ] Validate every value in code, and reject what falls outside the format
- [ ] Recheck the important criteria on the raw text
- [ ] Limit input size without treating that limit as protection against injection
- [ ] Bound actions in code and check the data that triggers them
- [ ] Require human approval for sensitive outgoing messages
- [ ] Block images and links the agent could generate toward the outside
What to remember
As soon as an agent reads what a stranger wrote, the stranger is talking to it. Your contact form becomes a prompt input, whether you like it or not.
A better prompt can help, but it is not enough. Limit access, check outputs and control sensitive actions. A correctly formatted answer can still be manipulated.
Plugging an agent into your mailbox, your CRM or your support desk? Let's talk.
Sources: Zenity Labs, SalesBleed ยท The Register, 24 September 2026 ยท OWASP, LLM01 Prompt Injection
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.