Skip to main content

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 explicit runInMode combined with a system-context trigger) that performs recordCreates, recordUpdates, or recordDeletes on 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 type apex or externalService whose URL element starts with http:// rather than https://, 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 type emailSimple or emailAlert whose 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> (or RecordBeforeSave) 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 Email action 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-status to 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 (000000000000000AAA and similar placeholders) and the 0SVx... / 0SDx... template Ids that flow generates internally.

Where to go next