Skip to main content

Apex

Vulkro walks every *.cls and *.trigger file in your project (SFDX force-app/main/default/classes/ and the legacy MDAPI src/classes/ layout, both), parses Apex via the dedicated Salesforce tree-sitter grammar, and runs the proprietary rule set. The detectors below cover eight of the twenty entries on the AppExchange Top-20 list (1, 2, 3, 4, 5, 7, 8, 18) plus the IDOR and mass-assignment shapes that the official checklist groups under CRUD / FLS but treats as separate failure modes.

What Vulkro detects

  • SOQL injection: untrusted input concatenated into Database.query(...) or any other dynamic SOQL string. Bind variables (:name) and String.escapeSingleQuotes(...) defuse the sink.
  • CRUD enforcement on writes: insert / update / delete / upsert without Schema.SObjectType.X.isCreateable() / isUpdateable() / isDeletable(), without WITH SECURITY_ENFORCED on the prior read, and without AccessLevel.USER_MODE on the DML.
  • FLS enforcement on reads: SOQL that returns restricted fields without Schema.SObjectField.X.isAccessible(), WITH USER_MODE, or a Security.stripInaccessible(AccessType.READABLE, ...) step before the value reaches the caller.
  • USER_MODE migration: USER_MODE (Spring '23+) is the platform-preferred enforcement mode. Vulkro emits a Low-severity advisory on legacy WITH SECURITY_ENFORCED so teams can plan the migration.
  • Sharing violations: classes declared without sharing (or with no sharing keyword) that perform reads or DML on user-owned sObjects. Such a class returns records the visiting user is not entitled to see.
  • IDOR: methods that take an Id parameter and SELECT by that Id without an OwnerId = :UserInfo.getUserId() (or equivalent) ownership filter.
  • Mass-assignment: JSON.deserialize(...) / JSON.deserializeUntyped(...) of user-controlled JSON into a sObject without an allowlist of writable fields.
  • Crypto misuse: Crypto.generateAesKey(128) (use 256), ECB mode, hard-coded IVs passed to Crypto.encrypt(...), MD5 / SHA-1 for password hashing.
  • Secrets in code: hard-coded API keys, OAuth client secrets, JWT signing keys, and database passwords embedded as Apex string literals.

Risk anchors

  • AppExchange Top-20 rule 1 (CRUD/FLS enforcement): the CRUD and FLS detectors above.
  • AppExchange Top-20 rule 2 (insecure software version): handled by the general vulkro SCA pipeline, not by an Apex-specific rule (managed packages with known CVEs surface through the same path).
  • AppExchange Top-20 rule 3 (sharing violation): the sharing-violation detector.
  • AppExchange Top-20 rule 4 (insecure storage of sensitive data): the secrets-in-code detector.
  • AppExchange Top-20 rule 5 (TLS / SSL configuration): callouts over http:// are flagged here when the URL is an Apex literal; Named Credential cleartext endpoints are flagged in the Named Credentials page.
  • AppExchange Top-20 rule 7 (CSRF): Apex REST methods that perform state changes without an Authorization-header gate.
  • AppExchange Top-20 rule 8 (stored and reflected XSS): the Apex side of the pair is a controller property that pulls from ApexPages.currentPage().getParameters().get(...) without escaping; the Visualforce sink is covered on the Visualforce page.
  • AppExchange Top-20 rule 18 (username or email enumeration): distinguishable error messages on login / password-reset Apex endpoints.

The IDOR and mass-assignment classes also overlap with OWASP API Security Top 10 entries API1 (Broken Object-Level Authorization) and API6 (Unrestricted Access to Sensitive Business Flows). The breach-class map traces each of these to a 2025-26 production incident where applicable.

Example positive (code that triggers a finding)

public without sharing class AccountSearchCtrl {
@AuraEnabled
public static List<Account> search(String name) {
String query =
'SELECT Id, Name FROM Account WHERE Name = \'' + name + '\'';
return Database.query(query);
}
}

Three findings fire on this class: SOQL injection (name is concatenated into the dynamic query string with no escape), sharing violation (the class is without sharing and reads a user-owned sObject), and FLS enforcement (the SOQL lacks WITH USER_MODE and the method does not strip inaccessible fields before returning the records).

Example negative (code that does not trigger)

public with sharing class AccountSearchCtrl {
@AuraEnabled
public static List<Account> search(String name) {
return [
SELECT Id, Name
FROM Account
WHERE Name = :name
WITH USER_MODE
];
}
}

The bind variable (:name) closes the SOQL-injection sink, with sharing makes the read run under the visiting user's record visibility, and WITH USER_MODE defers CRUD and FLS enforcement to the platform. None of the rules emit.

Tuning

  • Confidence is High when the source-to-sink path is direct (a method parameter or currentPage().getParameters() read flows into the sink in the same expression). Medium when the source is an instance field set elsewhere in the class. Low when the source is a static field initialised at class load.

  • Common false positives: dynamic SOQL where the variable part is a whitelisted column name (fieldsToQuery built from an enum). Wrap the user-pickable token in a small helper that returns one of a closed set of Schema.SObjectField values, or suppress with a reason="..." directive.

  • Suppress an individual finding inline:

    // vulkro:disable-next-line APEX_SOQL_INJECTION reason="column whitelist enforced at line 12"
    return Database.query(query);

    Suppress for an entire file with // vulkro:disable-file APEX_SOQL_INJECTION reason="...". Project-wide fingerprints live in .vulkroignore.

Where to go next