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
GenAiFunctionwhose<invocationTarget>references an Apex class declaredwithout 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.clssource paths and their sharing posture; pass two walks everygenAiFunction-meta.xmland 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, andGenAiFunctiondeclarations. 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
GenAiPromptTemplatethat 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 relevantGenAiPromptTemplateschema (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
vulkroscanner 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'smcp-auditcommand 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.xmland 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-01public 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/@AuraEnabledaction reads a PII free-text field (Noma's vector wasLead.Descriptionat 42,000 characters; the catalog also coversCase.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, aPattern.matchesallowlist, orSecurity.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, orlocalhost). - Agent action on a PII object without with-sharing (AP-064). An
Apex class that exposes an agent-action surface
(
@InvocableMethodor@AuraEnabled) over a PII-bearing sObject carries neitherwith sharingnorwithout 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.VerifyCustomeraction (or any sibling from the rules-bundle catalog) but is missing theattributeMappingsblock for the action's required outputs (isVerifiedandcustomerId, both asattributeType=StandardPluginFunctionOutputandmappingType=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.xmltest file. The patterns are configurable via the rules bundle.
Where to go next
- Apex detectors for the
without sharingposture rule that pass-one of the ForcedLeak walk depends on. - Cross-method engine for the AP-018/024/042/044/060 cross-method auth, DML, callout, and dynamic-dispatch checks.
- SFGE-gap pack for the three findings that cover Apex constructs Salesforce's Graph Engine documents as out of scope.
- Rules-bundle distribution for how the catalog above stays current without a binary release.
- Profiles and permission sets for the identity layer that controls which users can talk to which agent.
- Methodology, breach-class map (ForcedLeak) for the 2025 incident timeline and the post-disclosure platform patch.