For Salesforce admins, architects and security reviewers who run sf-audit or are deciding whether to. This covers what 1.17 checks that 1.16 did not, and one finding type that changed meaning.
TL;DR
- 14 new read-only checks cover surfaces most org reviews skip: inbound email services, Platform Event subscribers, Data Cloud data spaces,
InstallHandlerscripts in packages, delegated admin groups, Partner Super Users and Restriction Rules. The tool now runs 109 checks. - Two new CRITICAL attack chains: an unauthenticated email that ends in privileged Apex, and a guest or portal user publishing a Platform Event that runs as Automated Process or an admin.
- Apex findings now name the control a class bypasses.
WITH SYSTEM_MODEdrops CRUD and FLS but leaves record sharing to the class's sharing keyword, so it is no longer reported as a CRITICAL sharing bypass. - CI support: SARIF output for GitHub code scanning, accepted-risk waivers that expire within 365 days, and no local history written on CI runners.
What You'll Learn
- Which Salesforce entry points let an outsider run code without logging in, and how sf-audit detects them
- Where privilege hides from a normal permission set review
- Why
WITH SYSTEM_MODEandwithout sharingare different findings, and how severities changed because of it - How to wire the audit into a pipeline without letting accepted risks fail every build
The Problem
Most Salesforce security reviews start from users, profiles and permission sets, then work outward to sharing and connected apps. That covers how people get in through the front door. It misses the side doors that never involve a login page.
An inbound email service runs Apex when a message arrives. A Platform Event subscriber runs when an event is published, often as Automated Process. A package's InstallHandler runs in system context on every install and push upgrade. None of these show up when you list who has Modify All Data.
Common questions this article answers:
- Can an anonymous outsider run Apex in my org without a login?
- Does
WITH SYSTEM_MODEmean my query ignores sharing? - Which admin-level access does a permission set query not show?
- How do I fail a CI build on new findings but not on risks we already accepted?
Quick Answer
Install or update the plugin and run it against a sandbox first:
sf plugins install @cclabsnz/sf-audit
sf audit security --target-org mySandbox
Everything is read-only: SOQL, Tooling API and REST GETs, plus Metadata API reads. The report lists each finding with a severity, the evidence it found and the compliance controls it maps to, then correlates findings into attack chains. If you already run 1.16, expect new findings in the areas below and some Apex findings to change id and severity.
The 14 New Checks
I grouped them by the question they answer rather than by API.
Can someone run code here without logging in?
| Check | What it looks for |
|---|---|
inbound-email-handler-exposure |
Email services that accept messages from any sender and run Apex |
platform-event-subscriber-context |
Platform Event subscribers running in system context or as an admin while low-trust or guest users can publish the event |
auth-provider-registration-handler |
A RegistrationHandler running as an admin that links accounts by email address, which opens the door to pre-auth account takeover |
delegated-authentication-exposure |
Delegated Authentication over unencrypted or non-mTLS endpoints, or assigned to System Administrator profiles |
oauth-device-and-asset-token-flows |
The OAuth Device Flow and Username-Password Flow enabled in OAuth and OpenID Connect Settings |
The OAuth check reads org settings only. If the audit cannot read a setting, it says so as an advisory instead of guessing "disabled". If you are planning for the Username-Password flow's retirement, the migration guide covers the timeline.
Who has admin power that a permission set query does not show?
| Check | What it looks for |
|---|---|
delegated-administration-groups |
Delegated admin groups that can manage users, log in as them or assign permission sets |
external-account-hierarchy-exposure |
Partner Super Users and External Account Hierarchies that widen portal record reach past the external org-wide default |
restriction-rules-integrity |
Inactive Restriction Rules leaving records exposed, and conflicting active rules on the same object |
Delegated administration is the one I see missed most. A delegated admin can reset passwords and assign permission sets for the users in their group, and none of that appears when you query PermissionSetAssignment for admin permissions.
What runs with more privilege than it should?
| Check | What it looks for |
|---|---|
package-post-install-supply-chain |
Packaged Apex implementing InstallHandler. Managed namespaces whose source is hidden are listed as an advisory |
flow-http-callout-credential-bypass |
Flow HTTP callouts with embedded API tokens, or callouts that go around Named Credentials |
genai-prompt-template-grounding |
Active Prompt Templates grounded on system-mode flows or unshared Apex, or mixing untrusted external fields with sensitive CRM data |
The prompt template check fits the same pattern as Agentforce agent user least privilege: an AI feature inherits whatever access its grounding runs with, and system mode grounding hands a user's prompt records they cannot open themselves.
Is the data plane outside core CRM locked down?
| Check | What it looks for |
|---|---|
data-cloud-data-space-isolation |
A default Data Space shared with integrations, partner users or agent users, and unconstrained Data Share Targets |
shield-tenant-secret-lifecycle |
Shield Platform Encryption tenant secrets not rotated in 365 days |
org-wide-email-spoofing |
Org-Wide Email Addresses available to every profile without SPF and DKIM alignment |
Two New Attack Chains
A finding on its own tells you a setting is wrong. A chain tells you that several findings together make a path an attacker can walk. sf-audit only reports a chain when every ingredient is present, so a clean org shows none.
Unauthenticated inbound email to privileged code execution (CRITICAL). An open email service address with no sender allowlist and no SPF/DKIM verification accepts mail from anyone, and the message triggers Apex or DML as a privileged user or in a class without sharing. The attacker needs your service address and nothing else.
Guest or low-trust Platform Event publish to automated process escalation (CRITICAL). A guest or portal user can publish a custom Platform Event, and a trigger or Flow subscribed to it runs as Automated Process or an admin. The publisher's own permissions and sharing stop mattering once the subscriber runs.
Either chain breaks if you fix any one step: restrict the email service's senders, take publish access away from the guest profile, or make the subscriber run as a user with only the access it needs.
The Apex Finding That Changed Meaning
Earlier versions reported WITH SYSTEM_MODE and AccessLevel.SYSTEM_MODE as a CRITICAL sharing bypass. That was wrong, and fixing it changed severities, so it is worth knowing before you compare reports.
System mode in SOQL and DML switches off the running user's object and field permissions. It does not switch off record sharing: which records the query can see is still decided by the class's sharing keyword. A with sharing class running a WITH SYSTEM_MODE query still respects sharing; it only stops checking CRUD and FLS.
So 1.17 reports these separately:
apex-crud-fls-system-mode(MEDIUM) for system-mode queries and DML, andguest-executable-apex-system-mode(HIGH) when the class is callable by guests- a CRITICAL
guest-executable-apex-unprotectedwhen a guest-callable class calls a helper declaredwithout sharing, naming the helper, because the helper's keyword governs while its code runs inherited sharingclasses no longer flagged CRITICAL as guest entry points, since at an entry point they run with sharing@RestResourceclasses without sharing reported alongside@AuraEnabledand@RemoteActionones
USER_MODE counts as CRUD and FLS enforcement. The audit also reads a class's sharing keyword from its declaration only, so a without sharing class that mentions "with sharing" in a comment is no longer read as protected.
This change was built with Intent, Spec, Test. Its three must-nevers are: never report a class as bypassing record sharing when its sharing is enforced; never report a guest-callable class as protected when it, or a helper it calls, is declared without sharing; and never let system mode pass as CRUD/FLS enforcement. Each has held-out tests that CI runs and the coding agent never sees.
A Bug Worth Admitting
The first version of the 14 checks passed every unit test and still could not see real evidence. Several checks reported a pass when their query failed. The tests were green because the mocks returned fields the real queries never selected. One Flow query asked for metadata that the bulk query cannot return, so formula scanning never ran.
The fix made each check read what the org actually returns, and made a failed read inconclusive instead of a pass. Every report now states its evidence coverage, the share of checks that gathered evidence. Below 90%, a would-be A grade is capped at B, and the report says why. A clean report from an audit that could not see half the org is worse than no report.
Running It in CI
1.17 adds three things for pipelines:
sf audit security --target-org ciSandbox --format sarif --output ./reports --fail-on HIGH
--format sarifwrites SARIF 2.1.0 for GitHub code scanning. Each result carries a stable fingerprint, so a recurring finding stays one alert instead of a new one per run.- Accepted-risk waivers (
--waivers, or.sfaudit-waivers.jsonin the working directory) take a reviewed finding out of the score and--fail-on. Matching is exact, a waiver runs at most 365 days, and a CRITICAL finding needsacknowledgeCritical. There is no wildcard to hide behind. - History is skipped by default when
CI,GITHUB_ACTIONS,TF_BUILDorJENKINS_URLis set, so shared runners keep no org data.--keep-historyturns it back on.
Frequently Asked Questions
Does sf-audit change anything in my org?
No. Every check is a read: SOQL, Tooling API, REST GETs and Metadata API reads. A unit test scans the check code and fails the build if a check tries to write.
Why did some of my Apex findings drop from CRITICAL after upgrading?
System-mode queries were reported as sharing bypasses before 1.17. They bypass CRUD and FLS, not sharing, so they are now MEDIUM (apex-crud-fls-system-mode), or HIGH for guest-callable classes. Classes that really run without sharing behind a guest entry point are still CRITICAL.
What does an advisory finding mean?
The audit could not read the setting through the APIs it uses, so it reports that as INFO rather than guessing. INFO findings carry no weight in the default scoring, so an advisory does not lower your grade unless you change the weights with --scoring-config. The OAuth flows check and some package checks produce them when settings or source are not readable.
Can I run it against production?
Yes, it only reads. Run it against a sandbox first to see what it reports and how long it takes. On a CI runner, skip the local history so no org data is left on the machine.
Key Takeaways
- Inbound email services, Platform Event subscribers and package
InstallHandlerscripts can all run code without anyone logging in. Review them like you review login access. - Delegated admin groups and Partner Super Users grant real power that permission set queries do not show.
WITH SYSTEM_MODEskips CRUD and FLS; the class's sharing keyword still decides record access. Treat them as separate risks.- A green test suite proves nothing if the mocks return data the real query never selects. Coverage reporting in the audit exists because of that.
What's Next?
- Install or update:
sf plugins install @cclabsnz/sf-audit - Read the full check inventory in docs/CHECKS.md and all 19 chains in docs/ATTACK-CHAINS.md
- If a guest user anomaly has already fired, start with the guest user anomaly investigation walkthrough
Resources & References
- sf-audit on GitHub and the v1.17.0 release notes
- @cclabsnz/sf-audit on npm, published with signed provenance
- Salesforce: Enforce User Mode for Database Operations
- Salesforce: Using the with sharing, without sharing, and inherited sharing Keywords
- Salesforce: Email Services
- Previous release write-up: sf-audit v1.0
Responses
Checking your session.
Loading responses.