T09 · Insecure Skill Coding Practices
- Location
infra/agents/nudger.prompt.ts:46- Finding
Indirect Prompt Injection Through Untrusted CRM Activity Content
- Content
View full analysis
Vulnerability Details
File Location:
infra/agents/nudger.prompt.ts:46-53; privileged capabilities are configured ininfra/agents/nudger.ts:47-61
Vulnerability Type: Indirect prompt injection into a tool-enabled agent
Risk Level: MediumVulnerable Code
infra/agents/nudger.prompt.ts:46-53:ts ## 3. Research each remaining deal Keep it short; this is a nudge, not an account brief. 1. The last activity: the most recent row in gtm_activities for the deal (subject and body). Quote the line that matters, with its date. 2. The workspace context: our positioning, known objections and competitors, so the reason and the draft speak to this deal. Write, for each deal: - **Why now**: one sentence, grounded in what you read: the close date that is about to slip, the next step nobody took, the objection left unanswered, or the fact that it has been flagged N Mondays running. - **Draft**: a follow-up email of at most four sentences the owner could send, picking up from the quoted last activity. No placeholders in brackets; if a fact is missing, write around it. It is a draft for the owner, never sent by you.infra/agents/nudger.ts:47-61:ts uses: [ { ref: gtmOpportunities, readOnly: true }, { ref: gtmAccounts, readOnly: true }, { ref: gtmActivities, readOnly: true }, { ref: dealNudges, readOnly: false }, { ref: slack.actions.postMessage, config: { // PLACEHOLDER: the channel the digests land in, as a Slack id (C…) // read from the connector's channel autocomplete. Locked so a digest // that quotes deal amounts never lands in a customer shared channel. channelId: "C0123456789", format: "markdown", disableUnfurling: true, }, }, ],Technical Analysis
The scheduled agent reads the
subjectandbodyof the latest activity and places their semantic content into the same model context used to deci ...[truncated 2862 chars]- Remediation
View remediation
Remediation Suggestions
- Explicitly classify all activity fields and workspace content as untrusted data in the system prompt. State that instructions, tool requests, policy claims, or role changes found in records must never be followed.
- Delimit retrieved fields using a structured representation such as JSON with fixed keys, and instruct the model to extract facts only from those fields.
- Separate content analysis from privileged actions. Use one restricted step to summarize activity text without Slack or write access, then pass only validated structured output to the posting step.
- Validate generated digests against an allowlisted schema before posting. Ensure every included deal ID belongs to the SQL-selected set and reject unexpected links, commands, mentions, or unrelated content.
- Enforce ledger integrity outside the model. Permit writes only for selected opportunity IDs, the computed ISO week, the corresponding owner, and a Slack timestamp returned by a successful post.
- Limit model reads to the minimum records and columns required for each selected deal rather than allowing unconstrained follow-up queries.
- Add adversarial tests containing prompt-injection strings in activity subjects and bodies, and verify that they cannot alter tool selection, expose unrelated records, change destinations, or create unauthorized ledger entries.
