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.

Where to go next