Your Apex Is Quietly Changing What It Returns: The v67.0 Security Shift

API version 67 changes Apex security defaults, sharing behavior, and SOQL clauses. This migration guide shows what to audit.

A restricted support user can run the same service for years and then receive a smaller result set after its class moves to API version 67.0. There may be no stack trace to investigate. The code still compiles; the platform now applies different security defaults.

Summer ‘26 changes several Apex security defaults when a class moves to API version 67.0. It lands when you increment your API version to 67.0 - not the day the release ships, but the day a class moves to the new version. The release includes three changes, including one that causes a compiler error. If you own Apex, read this before you touch sf project deploy, because your existing code is about to change in ways that don’t announce themselves.

Database operations default to user mode

This change explains the reduced result set. Historically, SOQL, SOSL, DML, and Database methods defaulted to system mode - they ran without enforcing the running user’s field-level security or object permissions. That’s why a restricted user saw more than they should have: the query wasn’t checking what the user should see, it was just running as the system.

From v67.0, those operations default to user mode. They enforce the running user’s FLS and object permissions. Broad integration user? Behavior may not change. Restricted standard user? Records and fields that used to come back now get filtered - or the query throws an access error that used to be silent. That second one is the quiet regression.

Wrote any query assuming system-mode access? You have a regression sitting in your org, waiting for the API bump to wake it.

Classes without a sharing declaration default to with sharing

The trap for handler classes. Previously, a class without an explicit sharing declaration defaulted to without sharing in most cases - an implicit bypass lots of code quietly relied on. From v67.0, that default flips to with sharing.

This is the one that bites the pattern every Salesforce developer knows: a trigger that does nothing but call a handler class, and the handler that forgot the sharing keyword because it never mattered before. Triggers always run in system mode, and always will. But the handler classes your triggers call are affected if they lack an explicit declaration. Huge surface area. The “implicitly without sharing” handlers will behave differently on v67.0 - less data, or access errors that used to be silent.

Here’s the shape of the fix:

// before v67.0  -  implicit, and it defaulted to without sharing
public class OpportunityHandler {
    public static List<Opportunity> getOpen(List<Id> ids) {
        return [SELECT Id, Amount FROM Opportunity WHERE Id IN :ids];
    }
}

// after v67.0  -  explicit, and now it enforces sharing
public with sharing class OpportunityHandler {
    public static List<Opportunity> getOpen(List<Id> ids) {
        return [SELECT Id, Amount FROM Opportunity WHERE Id IN :ids];
    }
}

Two words added. Entire behavior changed. That’s the whole story of this release.

WITH SECURITY_ENFORCED is removed

This change breaks compilation. WITH SECURITY_ENFORCED is entirely removed in v67.0, replaced by WITH USER_MODE. Any class still using the old clause will not compile. Not syntactic. A security shift. The platform is forcing the intent into the open.

// before  -  removed, will not compile on v67.0
List<Account> accts = [SELECT Id, Name FROM Account WITH SECURITY_ENFORCED];

// after  -  explicit user mode
List<Account> accts = [SELECT Id, Name FROM Account WITH USER_MODE];

// when a query genuinely needs system access
List<Account> accts = [SELECT Id, Name FROM Account WITH SYSTEM_MODE]; // why: setup validation

Declare the access mode in new code. WITH USER_MODE when you want the running user’s permissions enforced. WITH SYSTEM_MODE when a query genuinely needs system-mode execution - and say why in a comment, because now you have to say it. The implicit-default era is over.

What to audit before moving to v67.0

Before you move a class to v67.0:

  1. Grep WITH SECURITY_ENFORCED. Every hit is a compile break waiting. Replace with WITH USER_MODE, or WITH SYSTEM_MODE plus a comment if there’s a real reason.
  2. Find every class without an explicit sharing declaration. The silent behavior changes. Add with sharing (or without sharing, deliberately) and verify against real users.
  3. Find every query that ran as a restricted user but expected system-mode results. Test both: the broad integration user, and a restricted standard user. The restricted-user test shows you exactly what’s about to stop coming back.

Run the grep in your repo root and you’ll see the scope immediately. WITH SECURITY_ENFORCED hits are mechanical. The sharing-declaration pass is the one that needs a human: you have to read each class and decide what it meant to do, not just what it did.

Managed packages increase the surface area

If you ship a managed package - or consume one - this gets more dangerous. Every class in a managed package has a locked-in API version, and the package owner controls the bump. If a managed package you rely on moves to v67.0 with classes that were relying on implicit without sharing, the behavior shift arrives in an upgrade, not a change you made. You can’t grep it. You can’t audit it. You just get it, along with a release note you may or may not read.

That’s a strong argument to review your dependency list before the Summer ‘26 wave of package upgrades, and to ask your package vendors what their v67.0 audit looks like.

Why audit before the version bump

You may not plan to increment API versions soon. The audit still matters because the changes don’t announce themselves at the bump. They surface as behavior drift in whatever code you touch after you’ve incremented. And by then you’re debugging a security-model change mixed in with your actual changes - the worst kind of bug hunt. Do the grep and the sharing-declaration pass while your code is still on v66.0. Then the only thing the bump changes is your intent, not your behavior.

Update the tests too

Your existing tests were written against the old security defaults. Tests that pass on v66.0 do not guarantee correct behavior on v67.0 - they were asserting behavior under system mode. You need new scenarios: run the query as a restricted user, assert the correct data visibility, before you upgrade the class. For managed packages, that review is nonnegotiable before deployment.

Make the access model explicit

This release requires an audit of code that may be years old. The useful question is: what security behavior was this code assuming? The god-class problem was always that behavior you can’t explain is behavior you can’t trust. v67.0 makes that concrete for every Apex developer on the platform. Audit the code now, add restricted-user tests, and make every access mode explicit before upgrading the class.

Sources