T09 · Insecure Skill Coding Practices
- Location
infra/agents/engager.ts:43- Finding
Untrusted inbound email can influence an autonomous agent with email-sending authority
- Content
View full analysis
Vulnerability Details
File Location:
infra/agents/engager.ts:43-59; related policy instructions ininfra/agents/engager.prompt.ts:32-38,46-51,75-80,107-113
Vulnerability Type: Prompt injection across an external-content trust boundary
Risk Level: HighVulnerable Code
ts systemPrompt: engagerPrompt, capabilities: ["memory", { slug: "context", config: { isReadOnly: true } }], uses: [ { ref: sendEmail, config: { mailboxUuid: rep.uuid } }, listEmailEvents, ], triggers: [ agentNativeTrigger({ agentSlug: "email", name: "status", config: { kinds: ["replied", "unsubscribed"] }, }), ], heartbeat: { intervalMinutes: 1440, maxMessages: 16, prompt: engagerHeartbeatPrompt, },The corresponding prompt relies on natural-language restrictions:
ts 1. **Native email trigger** — inbound status. It fires when the thread's status becomes `replied` (they wrote back) or `unsubscribed` (they opted out). That is the event half of the loop. 2. **Heartbeat** — silence. A thread that stays at `sent` never fires the trigger. The heartbeat re-wakes this chat so you can call `listEmailEvents` and decide. It is a status check, not a reason to invent a first send or to run a sequence.ts Read the workspace context (ICP, product, objections, proof) before you claim anything about what you sell. A fact that is not in context, not in this thread, and not something the lead just told you is something you do not say. On a heartbeat, or whenever this chat does not already carry a fresh event payload, call `listEmailEvents` with this thread's uuid before you decide.ts Call `sendEmail` once. The mailbox is already bound; do not pass a different one. For any message that continues a thread — a wake from the native email trigger, or a heartbeat follow-up on a thread you already started — pass: - `inReplyTo`: the Messag ...[truncated 3370 chars]- Remediation
View remediation
Remediation Suggestions
- Place a deterministic authorization wrapper in front of
sendEmailrather than exposing the unrestricted native action directly to the model. - Derive and lock the recipient, thread UUID,
inReplyTo, andreferencesvalues from the authenticated trigger payload. Do not allow the model or email body to supply or replace these fields. - Reject attempts to add recipients, change recipients, start a new thread, or send outside the triggering conversation.
- Enforce the one-send-per-wake rule and follow-up limit in code or platform policy rather than only in the system prompt.
- Separate inbound email content from trusted instructions using structured fields, and clearly label the body as untrusted data that must never define tool parameters or authorization decisions.
- Expose only an allowlisted, minimal subset of workspace context to externally triggered runs. Keep secrets, credentials, internal instructions, and unrelated customer data unavailable.
- Disable persistent memory writes for reply-triggered and heartbeat-triggered executions unless they are strictly required. If memory writes are needed, validate them against a schema and reject instruction-like content derived from inbound email.
- Add policy tests using adversarial email replies that attempt recipient changes, context extraction, additional sends, suppression bypasses, and memory modification. Verify that the tool layer rejects these actions independently of model behavior.
- Place a deterministic authorization wrapper in front of
