Skip to main content

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

FindingEngine questionMarker vocabulary
Invocable / AuraEnabled validationDoes any reachable helper carry a guard?if (...), String.isBlank, Pattern.matches, Schema.sObjectType.X.fields.Y.isAccessible()
REST endpoint authDoes any reachable helper check identity?UserInfo, Auth., FeatureManagement, PermissionSetAssignment, getSessionId(, checkPermission(
Partial-success DMLDoes any reachable helper inspect the SaveResult?.getErrors(), .isSuccess(), .getId(), helpers named processSaveResults / handleSaveResults / assertSavedOk / auditSaveResults
HTTP callout retryDoes 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:

  1. A method body calls Type.forName(<identifier>) where the argument is a runtime variable (not a quoted string literal), and
  2. NO method body anywhere in the workspace carries an FLS, CRUD, or sharing marker, AND
  3. NO class declaration anywhere in the workspace declares with sharing or inherited 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); the VULKRO_SF_AP060_LEGACY=1 env var instead silences emission.

  • The engine treats @isTest classes 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.