Skip to main content

Agentforce and AI

Vulkro walks Agentforce metadata under force-app/main/default/genAiAgents/, genAiPlugins/, genAiFunctions/, and genAiPromptTemplates/ (plus the matching MDAPI layout), and cross-references each agent action against the Apex class it targets. The headline finding here is ForcedLeak, the September 2025 CVSS 9.4 class-bypass that affected every Agentforce deployment where an agent action was backed by a without sharing Apex class.

What Vulkro detects

  • ForcedLeak (CVSS 9.4): a GenAiFunction whose <invocationTarget> references an Apex class declared without sharing (or with no sharing keyword). The agent's runtime user executes the action, and because the class runs in system context the agent leaks records from every visiting user's data to every other user that talks to the agent. Vulkro implements a two-pass walk: pass one collects all .cls source paths and their sharing posture; pass two walks every genAiFunction-meta.xml and resolves its Apex action target to a row in the pass-one table. A finding emits when the target class row says "without sharing" or has no sharing keyword.
  • Agentforce agent inventory: a flat list of GenAiAgent, GenAiPlugin, and GenAiFunction declarations. Not a finding by itself: this is the governance baseline a security team uses to answer "what agents are deployed in this org and what actions can they reach?" Vulkro emits these at informational severity so the answer lives in the same SARIF as the rest of the scan.
  • GenAiPromptTemplate grounding source trust: a GenAiPromptTemplate that pulls grounding data from a knowledge source whose trust posture is not pinned. The metadata shape for this rule is deferred: we do not yet emit until the relevant GenAiPromptTemplate schema (the <retrievers> block's <source> element) is publicly verifiable against the platform's actual contract. The rule lives in the codebase behind a feature flag and is exercised against synthetic fixtures.
  • MCP server endpoints exposed via Agentforce: an agent action that delegates to an MCP server. Because the general vulkro scanner already inspects MCP server manifests for the catalog of MCP-specific issues (unpinned npx, mutable git refs, overbroad filesystem root, inline env secrets, cleartext endpoints), this rule cross-references the MCP audit results rather than re-implementing the checks. See the general scanner's mcp-audit command for the full MCP rule list.

Risk anchors

  • 2025-26 breach class map: ForcedLeak. CVSS 9.4. Disclosed September 2025. The vulnerability is the class-bypass shape described above: an attacker who can prompt the agent (any visiting user) extracts records they are not entitled to see because the Apex action class is without sharing. This is the highest-severity Agentforce finding Vulkro emits.
  • AppExchange Top-20 rule 20 (Password Echo): not directly an Agentforce shape, but the governance pattern is the same: a public surface (the agent) reading from an under-privileged backend bypasses the visibility model. The ForcedLeak rule is the Agentforce manifestation of that class.

Example positive (code that triggers a finding)

<!-- LeadLookup.genAiFunction-meta.xml -->
<GenAiFunction xmlns="http://soap.sforce.com/2006/04/metadata">
<masterLabel>Lead Lookup</masterLabel>
<invocationTarget>LeadLookupAction</invocationTarget>
<invocationTargetType>apex</invocationTargetType>
</GenAiFunction>
// LeadLookupAction.cls
public without sharing class LeadLookupAction {
@InvocableMethod
public static List<Result> run(List<Request> requests) {
List<Result> out = new List<Result>();
for (Request r : requests) {
out.add(new Result(
[SELECT Id, Name, Email FROM Lead WHERE Status = :r.status]
));
}
return out;
}
}

Vulkro's pass-one walk records LeadLookupAction.cls as without sharing. Pass two reads the <invocationTarget> from LeadLookup.genAiFunction-meta.xml, resolves it to the pass-one row, sees the sharing posture, and emits ForcedLeak (Critical). Every visitor who can prompt this agent receives Lead records the visitor is not entitled to see in the UI.

Example negative (code that does not trigger)

// LeadLookupAction.cls
public with sharing class LeadLookupAction {
@InvocableMethod
public static List<Result> run(List<Request> requests) {
List<Result> out = new List<Result>();
for (Request r : requests) {
out.add(new Result(
[
SELECT Id, Name, Email
FROM Lead
WHERE Status = :r.status
WITH USER_MODE
]
));
}
return out;
}
}

with sharing runs the SOQL under the visiting user's record visibility, WITH USER_MODE enforces CRUD and FLS, and the agent action now respects the platform's authorisation model. The GenAiFunction metadata is unchanged. The ForcedLeak rule does not emit; the agent-inventory line still appears at informational severity as the governance baseline.

Tuning

  • ForcedLeak is Critical with High confidence whenever the two-pass cross-reference succeeds: the rule only emits when both files (the genAiFunction-meta.xml and the .cls) are present and the resolution is unambiguous. There is no "Low" or "Medium" tier on this finding.

  • Common false positives: an Apex action class that genuinely needs to bypass sharing because it audits records on behalf of a security team. In that case, document the reason inline:

    // vulkro:disable-file FORCEDLEAK_AGENTFORCE reason="audit role, restricted to SOC permset" until=2026-12-01
    public without sharing class SecurityAuditAction { ... }

    The until= qualifier expires the suppression so the finding re-emerges if the class outlives the documented intent.

  • Agent-inventory rows are informational and do not count toward the scan's exit-code gate; suppressing them is not necessary unless you specifically want them out of the SARIF.

ForcedLeak chain pack (v0.1.3)

The original ForcedLeak rule catches the original class-bypass shape. v0.1.3 adds four sibling findings that catch the rest of the ForcedLeak attack chain Noma Security demonstrated against Agentforce in late 2026:

  • PII free-text into agent context (AP-061). An @InvocableMethod / @AuraEnabled action reads a PII free-text field (Noma's vector was Lead.Description at 42,000 characters; the catalog also covers Case.Description, Contact.Description, Account.Description, and siblings) and returns the value to the agent context with no sanitization. Recognised sanitizer markers: String.escapeHtml4, String.stripHtml, a length bound via .abbreviate / .left / .substring, a Pattern.matches allowlist, or Security.stripInaccessible. The PII field catalog is part of the signed rules bundle.
  • Over-broad / expired Trusted URL (AP-062). A Trusted URL entry matches one of the wildcard patterns Salesforce removed from the default Agentforce allowlist in the Feb 28 2026 announcement (*.salesforce.com, *.force.com, *.cloudforce.com, *.visualforce.com, *.lightning.com, plus the obvious siblings). The wildcard catalog is part of the signed rules bundle.
  • Trusted URL grants img-src / connect-src / frame-src / script-src / style-src / media-src to an external host (AP-063). A Trusted URL entry hands a CSP directive to a host that is not obviously org-owned (the heuristic: domain does not end in .lightning.force.com, .my.salesforce.com, .documentforce.com, .visualforce.com, .cloudforce.com, or localhost).
  • Agent action on a PII object without with-sharing (AP-064). An Apex class that exposes an agent-action surface (@InvocableMethod or @AuraEnabled) over a PII-bearing sObject carries neither with sharing nor without sharing. The implicit system-mode default lets the agent see records the caller's profile would never see directly.

Clayton-parity pack (v0.1.3)

Five new findings cover the planner / topic / action metadata mistakes that produce wide-open agent actions, in line with the rules the major third-party reviewers ship:

  • Missing customer verification on a PII agent (AP-066). A planner mentions a PII-bearing standard object (Contact, Lead, Case, Account) but wires no customer-verification action. This is the failure shape Noma demonstrated in ForcedLeak's Web-to-Lead variant: the agent answers questions about Lead records without first proving the caller is entitled to see those records.
  • Missing output assignment for VerifyCustomer (AP-067). A planner references the standard SvcCopilotTmpl__ServiceCustomerVerification.VerifyCustomer action (or any sibling from the rules-bundle catalog) but is missing the attributeMappings block for the action's required outputs (isVerified and customerId, both as attributeType=StandardPluginFunctionOutput and mappingType=Variable).
  • Insufficient topic instructions (AP-068). A topic's <masterLabel>, <description>, or <scope> word counts fall below the documented minimums (5 / 15 / 15 by default; configurable via the rules bundle).
  • Excessive topics per planner (AP-069). A planner has more than the catalog threshold of topics (default 15). Above that ceiling, routing accuracy starts to degrade and the agent picks the wrong topic for the question it was asked.
  • Untested Agentforce action / topic / agent (AP-070). An agent, topic, or action in production metadata has no paired *.aiTestCase-meta.xml, *.botTest-meta.xml, or *.agentTest-meta.xml test file. The patterns are configurable via the rules bundle.

Where to go next