Cross-method Apex taint engine
Most static Apex scanners ask a single question of a single method: "does THIS method check auth before its DML?" That works for self- contained code, but it mis-fires on the most common Apex shape in production: the entry method delegates the check to a sibling helper, the sibling delegates the read to another sibling, and so on. The scanner cannot see the marker that sits two hops away, so the entry gets flagged even though the auth check is real.
Vulkro's cross-method engine sidesteps that whole class of false- positives. The engine builds the workspace-wide Apex call graph at the start of every scan, then asks the same question of the graph instead of the method: "does ANY method reachable from the entry carry the marker?"
What the engine covers
| Finding | Engine question | Marker vocabulary |
|---|---|---|
| Invocable / AuraEnabled validation | Does any reachable helper carry a guard? | if (...), String.isBlank, Pattern.matches, Schema.sObjectType.X.fields.Y.isAccessible() |
| REST endpoint auth | Does any reachable helper check identity? | UserInfo, Auth., FeatureManagement, PermissionSetAssignment, getSessionId(, checkPermission( |
| Partial-success DML | Does any reachable helper inspect the SaveResult? | .getErrors(), .isSuccess(), .getId(), helpers named processSaveResults / handleSaveResults / assertSavedOk / auditSaveResults |
| HTTP callout retry | Does any reachable helper carry a retry shape? | for / while / catch block, helpers named withRetry / executeWithRetry / retryUntil / retryWrapper |
Dynamic-dispatch (Type.forName(<variable>).newInstance()) | Does ANY class in the workspace declare with sharing? | per-method body: isUpdateable( / isAccessible( / WITH SECURITY_ENFORCED / Security.stripInaccessible / AccessLevel.USER_MODE; per-class header: with sharing class / inherited sharing class |
The marker check is case-insensitive substring matching, so a method
that uses a renamed helper (AccessControlService.assertCanRead(),
SecurityAuditor.verifySession(), etc.) earns coverage as long as the
helper's body itself carries a recognised marker. The check is
intentionally permissive: the actionable failure mode is "NO method
anywhere in the workspace checks auth at all," which is the real
breach risk.
Dynamic-dispatch: the AP-060 finding
The cross-method engine adds one new finding in v0.1.3 that has no intra-file analogue.
A method that calls
Type handlerType = Type.forName(handlerName);
IHandler h = (IHandler) handlerType.newInstance();
h.process(payload);
asks the platform to instantiate a class by name at runtime. The class
that ends up running is decided by the value of handlerName, which
the platform's Graph Engine cannot resolve statically. The downstream
class's DML, callouts, and identity checks are invisible to the
platform scanner. If handlerName is attacker-influenced (for example,
read from an @InvocableMethod request, an LWC @AuraEnabled call,
or a REST body), the attacker picks which class executes against the
org's records.
Vulkro emits AP-060 when:
- A method body calls
Type.forName(<identifier>)where the argument is a runtime variable (not a quoted string literal), and - NO method body anywhere in the workspace carries an FLS, CRUD, or sharing marker, AND
- NO class declaration anywhere in the workspace declares
with sharingorinherited sharing.
The class-header check matters because idiomatic Apex declares
public with sharing class Foo { ... } at the class level and never
repeats the marker per method. Without the header check the detector
would mis-fire on the most common shape in production. without sharing class is intentionally excluded from the coverage check:
that is exactly the unsafe shape AP-060 is built to catch.
Type.forName('LiteralName') (a string literal) stays silent because
the developer pinned the class at compile time and the Graph Engine
can resolve the dispatch.
The empirical evidence that the platform's Graph Engine cannot resolve
the dynamic shape ships in the repository at
bench/sfge-comparison/results-20260608-162025/. Code Analyzer
v5.13.0 with all seven SFGE rules selected returned zero findings on
both ap060-type-forname.cls (the entry method) AND
Ap060ConcreteHandler.cls (the runtime handler), so the gap is
documented against a real platform release rather than asserted.
Tuning
-
Every cross-method finding in this family carries a one-release fallback to its prior intra-file detector via an environment variable:
VULKRO_SF_AP018_LEGACY=1,VULKRO_SF_AP024_LEGACY=1,VULKRO_SF_AP042_LEGACY=1,VULKRO_SF_AP044_LEGACY=1. AP-060 has no intra-file legacy detector (it ships fresh in v0.1.3); theVULKRO_SF_AP060_LEGACY=1env var instead silences emission. -
The engine treats
@isTestclasses as out of scope. A test method that does its own auth check does not earn coverage for the production entry; a test method that omits a check does not get flagged as a missing check. -
A call to a method in an external managed package (
acme__SecurityHelper.assertCan(...)) cannot be walked because the implementation is not in the workspace. The engine treats the external call as opaque: it neither grants nor denies coverage. A team that delegates auth to a managed-package helper today should document the delegation inline:// vulkro:disable-next-line AP-044 reason="auth delegated to acme__SecurityHelper"public static String getLead(Id leadId) { ... }
Where to go next
- Apex detectors for the per-file Apex rule catalog (AP-001..AP-058, AP-061..AP-070), which runs alongside the cross- method engine on every scan.
- Agentforce and AI for the AP-061..AP-064 ForcedLeak pack and the AP-066..AP-070 Clayton-parity pack, which both ride the per-file dispatch instead of the cross-method engine.
- Methodology for the full scanner architecture.