Autopsy 15 · SaaS agent context boundary |  Comic Strip 3 · Forced Lead

Would Ethosure have caught this?  Salesforce Agentforce’s ForcedLeak indirect prompt injection

Noma Labs’ Sasi Levi demonstrated a CVSS 9.4 chain in Salesforce Agentforce: a malicious Web-to-Lead submission stored 42,000 characters of injected instructions in the CRM, then exfiltrated data through a rendered image on an expired trusted domain. Salesforce patched via Trusted URLs Enforcement on September 8, 2025. Tenant-owned CRM records were the prompt-injection vector. The fix is policy on the agent, not just on the domain allowlist.

Series: Would Ethosure have caught this?
Root cause: SaaS agent treats tenant-owned record content as trusted prompt context
Coding-ethos policy: agent.crm_context.record_content_untrusted_by_default

The incident

Noma Labs researcher Sasi Levi discovered and reported ForcedLeak to Salesforce on July 28, 2025. Salesforce acknowledged the report on July 31 and rolled out Trusted URLs Enforcement for Agentforce and Einstein AI on September 8. Noma disclosed publicly on September 25 at CVSS 9.4. The attack chain: an external attacker submits a Web-to-Lead form (unauthenticated by design) with hidden prompt-injection instructions in the Description field, which accepts up to 42,000 characters compared with 40 characters for Company and 80 for Email. When an internal employee later asks Agentforce a normal question about the lead, the agent reads the attacker-controlled record and treats the embedded instructions as part of its task. The payload asks Agentforce to query CRM data (lead counts, email addresses) and then render an image tag with the answers URL-encoded into the query string of cdn.my-salesforce-cms.com. The exfiltration domain had previously been on Salesforce’s Content Security Policy allowlist. Salesforce’s post-incident control — Trusted URLs Enforcement — tightens the domain allowlist. It does not close the general class of indirect prompt injection through tenant-owned CRM records.

READ  Would Ethosure have caught this? Sysdig’s LLM-driven post-exploitation in four pivots

The applicable coding-ethos policy

Policy packRegulated-enterprise base pack (with model / provider governance overlay)
Principletenant-content-is-untrusted-by-default
Control familyCRM context sanitisation + rendered-content egress gate

Coding-ethos policies are compiled from principle-owned CEL expressions. Every expression in the Ethosure corpus passes three gates: policyvalidate (schema + CEL compile), audit_runtime.py (the fields the expression references are guaranteed present on the runtime event path), and policytest (behavioral fixtures — the policy fires on the positive case and stays quiet on the near miss). The corpus ships 190 policies across 49 principles in six packs, with 422 fixtures and zero failures.

Walkthrough

  1. The Salesforce fix — tightening the CSP allowlist so the exfiltration domain no longer resolves — is the right response for this specific payload. It is not the right response for the class of attack. Any SaaS agent that reads tenant records as trusted prompt context can be steered by whoever can write into those records, and Web-to-Lead is unauthenticated by design. The boundary that matters is between record content and agent instructions.
  2. agent.crm_context.record_content_untrusted_by_default fires on PreToolUse for any agent whose prompt is assembled from CRM record content that was originally written by an external, unauthenticated identity, unless the content has passed through a sanitisation pipeline with a recorded lease. Severity block. Web-to-Lead submissions land in that category by default.
  3. agent.rendered_content.egress_via_image_url_requires_data_class_check is the second expression. Data exfiltration through image URLs is the classic indirect-prompt-injection egress vector; it worked for ForcedLeak, EchoLeak, and most of the public agent-exfil demonstrations of the past year. The expression fires on any agent response that includes a URL parameter whose value matches a data-classifier hit on CRM record content. Severity block. The trusted-URL allowlist becomes belt-and-braces.
  4. agent.context_source.field_length_asymmetry_gate is the third. Salesforce’s Description field accepts 42,000 characters while Company is 40 and Email is 80. A prompt-injection payload cannot hide in an 80-character field. The expression fires on any prompt assembly whose largest single field exceeds a per-object threshold declared by the tenant’s data-steward. Severity warn, escalating to block if the field contains inline instructions the sanitiser flags.
READ  Would Ethosure have caught this? The Sentry MCP server SSRF

Policy YAML

# Pack: regulated-enterprise-base (v2)
# Overlay: model-provider-governance
principles:
  - id: tenant-content-is-untrusted-by-default
    title: Tenant Content Is Untrusted By Default
    directive: >-
      SaaS agents must treat tenant CRM records authored by external identities as
      untrusted prompt input, must gate rendered-content egress against data-classifier
      hits, and must surface field-length asymmetries that hide prompt-injection payloads.
    policy:
      expressions:
        - id: agent.crm_context.record_content_untrusted_by_default
          scope: agent_action
          severity: block
          event: PreToolUse
          principle_ids:
            - tenant-content-is-untrusted-by-default
            - security-by-design
          skill_id: safe-crm-agent-context
          when: >
            prompt.assembled_from.exists(s,
                 s.source_kind == "crm_record"
                 && s.record.authored_by.identity_kind == "external_unauthenticated"
                 && !s.sanitisation_lease.exists(l, l.state == "active"))
          message: >-
            Agent prompt is being assembled from a CRM record authored by an external
            unauthenticated identity with no recorded sanitisation lease.
          advice: >-
            Route the record through the prompt-sanitisation pipeline. Web-to-Lead
            submissions are the ForcedLeak vector; treat them as untrusted by default.

        - id: agent.rendered_content.egress_via_image_url_requires_data_class_check
          scope: agent_action
          severity: block
          event: PreToolUse
          principle_ids:
            - tenant-content-is-untrusted-by-default
            - radical-visibility
          when: >
            tool_call.category == "render_content"
            && response.rendered_urls.exists(u,
                 u.query_params.exists(p,
                      dlp_facts.exists(f, f.matches(p.value)
                                        && f.classification != "public")))
          message: >-
            Agent-rendered content includes a URL whose query string contains a data-
            classifier hit on non-public content.
          advice: >-
            Reject the render or strip the parameter. This is the exfiltration primitive
            behind ForcedLeak and every subsequent image-URL leak.

        - id: agent.context_source.field_length_asymmetry_gate
          scope: agent_action
          severity: warn
          event: PreToolUse
          principle_ids:
            - tenant-content-is-untrusted-by-default
            - radical-visibility
          when: >
            prompt.assembled_from.exists(s,
                 s.source_kind == "crm_record"
                 && s.field.length > tenant.field_length_thresholds
                                       .for_object(s.field.object).max_chars)
          message: >-
            CRM record field exceeds the tenant-declared length threshold for its object.
          advice: >-
            Surface the asymmetry to the data-steward. A 42,000-character Description field
            when Company is 40 characters is where prompt-injection payloads hide.

Assurance boundary. Fixtures prove that synthetic positive cases fire and near-misses stay quiet. They do not establish production alert rates. DLP-backed controls depend on upstream detection, shell-command regexes are defense in depth rather than a sandbox boundary, and regulatory tags are engineering aids requiring counsel review.

Where this policy lives in the corpus

The Ethosure importable corpus ships six coding-ethos packs. Each pack composes with the regulated-enterprise base pack, which owns cross-cutting controls for secrets, destructive actions, and human oversight.

  • Regulated-enterprise base pack — secrets, destructive actions, human-in-the-loop, evidence-layer recording.
  • Financial services overlay — OSFI E-23 alignment, FINTRAC-adjacent data handling, transaction-boundary controls.
  • Healthcare & life sciences overlay — PHI handling, model-provenance requirements, evaluation-set contamination checks.
  • Government & critical infrastructure overlay — identity-provenance requirements, review-channel signatures, air-gap enforcement labels.
  • Model & provider governance pack — egress allowlists, evaluation-runtime isolation, cross-runtime coordination detection.
  • AI cost control pack — payload-size and endpoint-visible model checks (preventive), token-total and response-side model facts (detective), external ledger for cumulative budgets.

Published September 3, 2026 · Ethosure · Part of the Autopsy Series.

Subscribe to Ethosure's Newsletter to get monthly updates on AI Governance

We don’t spam! Read our privacy policy for more info.