Flow
Vulkro parses every *.flow-meta.xml (SFDX) and *.flow (legacy MDAPI)
file in the project. Flow is the AppExchange Security Review's
fastest-growing failure category: declarative tooling lets an admin
ship business logic that bypasses sharing rules without ever touching
Apex, and the Security Review's existing Apex-focused checklist does
not catch most of those shapes. Vulkro's Flow detectors close that gap.
What Vulkro detects
- System-context DML: an auto-launched flow declared
<runInMode>SystemModeWithoutSharing</runInMode>(or, on older flow XML, the absence of an explicitrunInModecombined with a system-context trigger) that performsrecordCreates,recordUpdates, orrecordDeleteson a user-owned sObject. The DML bypasses every sharing rule on the org. - Loop DML: a
<loops>block whose body contains a<recordCreates>,<recordUpdates>, or<recordDeletes>action. Each iteration counts against the per-transaction DML governor limit, and the per-record locking footprint multiplies. Bulkify by appending to a collection variable inside the loop and issuing a single DML action after the loop closes. - Hardcoded record IDs: 15- or 18-character Salesforce Ids embedded as literal values in
<filters>,<inputAssignments>, or<conditions>. The Id binds the flow to a specific org and breaks on deploy. - Insecure REST callout: an
<actionCalls>of typeapexorexternalServicewhose URL element starts withhttp://rather thanhttps://, or whose URL is built from a flow input variable that may be user-controlled (SSRF: the flow's runtime user gets to call into your internal network). - PII in error emails:
<actionCalls>of typeemailSimpleoremailAlertwhose body element references a merge field on a sObject that the PII map marks as tier-1 or tier-2. - Missing entry criteria on record-triggered flows: a
<start>block with<triggerType>RecordAfterSave</triggerType>(orRecordBeforeSave) that lacks a<filters>element. The flow runs on every record change on the object: a governor-limit and recursion-loop risk plus a performance regression.
Risk anchors
- AppExchange Top-20 rule 6 (sensitive information in debug): a Flow
Send Emailaction with PII in the body is the declarative equivalent; the PII-in-error-emails detector targets it. - AppExchange Top-20 rule 14 (Aura components: CSS outside component): mapped to LWC / Aura; the Flow rule that touches the same category is the auto-launched flow with no entry criteria (an unrestricted business-logic surface).
The system-context DML detector specifically targets the Top-20
vulnerability #3 (sharing violation) as it manifests in Flow rather
than Apex. This is the failure mode that ships through the Security
Review more often than the Apex equivalent because the
SystemModeWithoutSharing setting is a single XML element with no
inline warning.
Example positive (code that triggers a finding)
<Flow xmlns="http://soap.sforce.com/2006/04/metadata">
<runInMode>SystemModeWithoutSharing</runInMode>
<start>
<triggerType>RecordAfterSave</triggerType>
<object>Lead</object>
</start>
<loops>
<name>updateLoop</name>
<collectionReference>Leads</collectionReference>
<iterationOrder>Asc</iterationOrder>
<nextValueConnector>
<targetReference>UpdateLead</targetReference>
</nextValueConnector>
</loops>
<recordUpdates>
<name>UpdateLead</name>
<inputReference>updateLoop</inputReference>
<connector>
<targetReference>updateLoop</targetReference>
</connector>
</recordUpdates>
</Flow>
Three findings fire: system-context DML (the flow runs in
SystemModeWithoutSharing and writes to Lead, a user-owned sObject),
loop DML (the recordUpdates action is invoked once per loop
iteration), and missing entry criteria (the start block has no
filters, so the flow fires on every Lead save).
Example negative (code that does not trigger)
<Flow xmlns="http://soap.sforce.com/2006/04/metadata">
<runInMode>DefaultMode</runInMode>
<start>
<triggerType>RecordAfterSave</triggerType>
<object>Lead</object>
<filters>
<field>Status</field>
<operator>EqualTo</operator>
<value>
<stringValue>Qualified</stringValue>
</value>
</filters>
</start>
<loops>
<name>collectLoop</name>
<collectionReference>Leads</collectionReference>
<iterationOrder>Asc</iterationOrder>
</loops>
<recordUpdates>
<name>UpdateLeadsBulk</name>
<inputReference>updatedLeadCollection</inputReference>
</recordUpdates>
</Flow>
DefaultMode runs the flow under the visiting user's sharing rules;
the entry filter restricts the trigger to Qualified leads; the
recordUpdates action operates on a pre-built collection variable
outside the loop body. None of the rules emit.
Tuning
- Confidence is High for system-context DML on any user-owned
standard sObject (Lead, Opportunity, Case, Contact). Medium on
custom sObjects (Vulkro does not know the org's sharing model on
custom objects without an org connection; tighten with
vulkro-sf org-statusto elevate to High). - Common false positives: a system-mode flow that genuinely needs to
write a record on behalf of every user (e.g. an audit-log append).
Suppress with a top-of-flow comment in a separate
<description>element and an inline directive in the matching rule profile, or accept the finding into the baseline. - Hardcoded record IDs ignore the platform's built-in org-default Ids
(
000000000000000AAAand similar placeholders) and the0SVx.../0SDx...template Ids that flow generates internally.
Where to go next
- Apex detectors for the Invocable Apex action shape that flows often delegate to.
- Methodology, section 1.1 for the Well-Architected Reliable dimension that loop DML and missing entry criteria sit under.
- Anti-patterns CLI for a per-Flow Well-Architected anti-pattern report.