Salesforce Flow security: system context, guests and Apex actions
Salesforce Flow security gets less attention than Apex security, even though a Flow can read, create, update and delete records with exactly the same reach. A Flow running in system context without sharing has no idea who started it. If the person who started it was an anonymous visitor to a public site, the Flow still does what it was built to do, with access to all data.
Flows are also easier to ship than Apex: no code review or test class is required, and the access decision is one setting in Flow Builder.
This post covers the Flow run modes and their defaults, screen flows exposed to guest users, Flows started from Apex and Lightning components, record-triggered flows that move data where it should not go, and what Vulkro for Salesforce checks in Flow metadata.
How does a Flow decide what it can access?
Every Flow runs in one of three contexts. Salesforce's Trailhead module on flow permissions describes them:
- User context: the Flow can access the objects, fields and records the running user can access.
- System context with sharing: the Flow can access all objects and fields, but record access follows the running user's sharing.
- System context without sharing: the Flow can access all objects, fields and records.
Note what "with sharing" means here. It enforces record visibility only. The Flow still reads and writes fields the user's profile hides. This is the same gap as with sharing in Apex, which we covered in with sharing vs without sharing.
The defaults depend on the Flow type:
| Flow type | Default context | Can be changed? |
|---|---|---|
| Record-triggered | System context without sharing | No |
| Schedule-triggered | System context without sharing | No |
| Screen flow | User context | Yes, to either system context |
| Autolaunched | The context of whatever started it | Yes, to either system context |
In metadata, the setting is the runInMode element on the Flow, with values such as SystemModeWithSharing and SystemModeWithoutSharing. A Flow with no runInMode, or the legacy DefaultMode, takes the default for its type, which is why reading the Flow type matters as much as reading the setting.
Who can run a Flow?
Salesforce requires the Run Flows permission (or the Flow User checkbox) to run Flows, and lets you restrict an individual Flow to specific profiles and permission sets. Salesforce has also been moving Flow access toward explicit grants, so check the Release Updates page in Setup for your org's status rather than relying on the older behaviour. The question for each Flow is simple to state: who holds access to run it, and from where can they start it?
Screen flows exposed to guest users
A screen flow placed on a public Experience Cloud page runs as the site's guest user. In user context that is fine: the guest's own, tightly limited access applies. In system context it is not. The Flow reads and writes with access to all data, on behalf of someone who never logged in.
The most dangerous shape is a guest-run Flow that lets the visitor choose which record it acts on:
<!-- Vulnerable: guest-run, system context, record chosen by the visitor -->
<Flow xmlns="http://soap.sforce.com/2006/04/metadata">
<processType>Flow</processType>
<runInMode>SystemModeWithoutSharing</runInMode>
<variables>
<name>recordId</name>
<dataType>String</dataType>
<isInput>true</isInput>
</variables>
<recordLookups>
<name>Get_Application</name>
<object>Application__c</object>
<filters>
<field>Id</field>
<operator>EqualTo</operator>
<value><elementReference>recordId</elementReference></value>
</filters>
</recordLookups>
</Flow>
recordId comes from the page URL or the component configuration, which the visitor controls. Change the id and the Flow returns someone else's application. This is the Flow version of an insecure direct object reference, and it is not stopped by guest sharing rules, because system context without sharing ignores them.
The fix is to let the platform enforce access, and to stop trusting the input:
<!-- Fixed: no system-mode run setting, so the screen flow runs in user
context and the guest's own sharing and field access apply -->
<Flow xmlns="http://soap.sforce.com/2006/04/metadata">
<processType>Flow</processType>
<variables>
<name>accessToken</name>
<dataType>String</dataType>
<isInput>true</isInput>
</variables>
<recordLookups>
<name>Get_Application</name>
<object>Application__c</object>
<filters>
<field>Access_Token__c</field>
<operator>EqualTo</operator>
<value><elementReference>accessToken</elementReference></value>
</filters>
</recordLookups>
</Flow>
A screen flow with no system-mode setting runs in user context, so the guest sees only what guest sharing rules and the guest profile allow. The record is found by a long random token the applicant received by email rather than by an id anyone can edit, and the lookup should return only the fields the page shows. If the Flow truly needs elevated access, prefer system context with sharing over without sharing, and keep the token check. The full guest hardening list is in our Experience Cloud guest user security checklist.
Flows called from Apex and Lightning components
Flows are entry points in more places than Flow Builder suggests:
- A Lightning component can embed a screen flow and pass it input variables. Those inputs are as untrusted as any
@AuraEnabledparameter. - Apex can start a Flow with
Flow.Interview. The Trailhead module above notes that autolaunched flows take the context of whatever ran them, so a Flow behaves differently depending on the caller. Check the context it runs in from each caller rather than assuming the Flow Builder setting is the whole answer. - A Flow can call Apex through an
@InvocableMethodaction. The Apex runs under its own class's sharing keyword, and it receives the Flow's variables, including anything a visitor typed into a screen. A user-context Flow that calls awithout sharinginvocable method has handed its caller system access. - AI agent actions can be backed by Flows. A Flow-backed action in system context reaches records the customer talking to the agent could never see.
The invocable method is often the weakest link, because it gets reused by Flows on internal, community and public pages alike.
Record-triggered flows and data exposure
Record-triggered flows always run in system context without sharing. That is usually what you want for automation, but it means they can move data across a sharing boundary without anyone noticing:
- Copying a sensitive field (salary, date of birth, a government id) onto a record that more users can see, such as a parent account or a public-facing case.
- Creating or updating records owned by, or shared with, external users.
- Sending an email or an outbound call that includes field values the triggering user could not read.
- Writing back a value from a field the user can edit into a field they should not control, such as an approval status or an admin flag.
These are not injection bugs. The review question is whether the destination's audience may see what the Flow put there.
A Salesforce Flow security checklist
- Know the run context of every active Flow, including those with no explicit setting.
- Use user context for screen flows unless there is a documented reason not to.
- Never let a system-context Flow select a record from an input the caller controls.
- List every Flow a guest or community profile can run, and the Apex actions each one calls.
- Give invocable Apex methods a sharing keyword and enforce object and field access in them.
- Review record-triggered flows that copy sensitive fields or send data outside the org.
- Add fault paths to every element that queries, writes or calls Apex.
What Vulkro for Salesforce checks in Flow metadata
vulkro-sf scan reads every *.flow-meta.xml (SFDX) and *.flow (Metadata API) file in the project. The Flow detectors page lists the checks. The security ones include:
flow-runinmode-elevated: a Flow in system context. It also lists Flows with the legacyDefaultModeor no run mode at all, because their real context depends on the Flow type, so the fixed screen flow above still appears there once for a reviewer to confirm.flow-runinmode-system-mode-recordcreates,-recordupdatesand-recorddeletes: each create, update or delete element in a system-mode Flow.flow-system-mode-sensitive-field-write: a system-mode Flow writing a sensitive-named field such as an SSN, a salary or an admin flag.flow-taint-source-to-sink: caller-influenced input (a screen field, an input variable or the triggering record) traced through a system-context Flow into a query filter or a write, with no validation on the way.flow-dynamic-filter-from-input: a Get Records filter whose value comes from an input variable.sf-guest-flow-input-record-selector: a system-mode screen Flow the site guest may run, where the record is chosen from caller input, as in the example above.sf-flow-apex-guest-reachable: a guest-runnable system-mode Flow whose Apex action has a CRUD or field-level security gap or awithout sharingrecord operation.sf-agent-isv-flow-system-context: an agent action backed by a system-mode Flow.
vulkro-sf readiness also lists Flow release-readiness items, such as screen flows that are not granted through a profile or permission set, and system-mode Flows whose Apex actions will start respecting user context.
When a guest or community profile can run the Flow, the finding joins the attack path from that user through the Flow and any Apex it calls to the object it touches. To see how Flows, triggers and Apex connect across the project, vulkro-sf flowmap draws the automation graph; see the flowmap docs.
One limit: these are metadata checks. Static analysis cannot see who started a Flow at runtime or which managed-package code calls it, and a missing grant in the project is not proof that nobody in the org can run it.
Where to see it
Run vulkro-sf scan ./force-app on your project, or open the VS Code extension to see Flow findings next to the Apex they call. For teams looking after many orgs, see Vulkro for Salesforce platform teams. For how a guest-run Flow fits into a full leak, read the anatomy of a guest user data leak. To get set up, start here.
Frequently asked questions
Do Salesforce Flows run in system mode?
It depends on the type. Record-triggered and schedule-triggered flows always run in system context without sharing. Screen flows default to user context, and autolaunched flows take the context of whatever started them, and both can be set to system context.
Does system context with sharing enforce field-level security?
No. It enforces record sharing only. The Flow can still read and write fields the running user's profile hides, so sensitive fields need their own controls.
Can guest users run Flows?
Yes, when a Flow is placed on a public site page and the guest profile has access to it. The Flow then runs as the guest user, which is safe in user context and risky in system context.
Are invocable Apex methods called from Flows a security risk?
They can be. The method runs under its own class's sharing keyword and receives the Flow's variables, which may include visitor input. Give it a sharing keyword and enforce object and field access inside it.
Sources
- Salesforce Trailhead, Grant Flow Permissions and Context
- Salesforce, Protecting your data: essential actions to secure Experience Cloud guest user access, March 2026
