← All posts
· 4 min read ·
SalesforceSecurityOWASPApexPythonOpen Source

Building apex-security-scanner: OWASP Static Analysis for Salesforce

How I built a Python CLI that runs OWASP Top 10 checks against Salesforce Apex code - detecting SOQL injection, hardcoded credentials, missing CRUD/FLS checks, and unsafe DML patterns.

Dark terminal screen showing code with security analysis output

Static analysis for Apex is an unsolved problem in most Salesforce shops. Salesforce’s built-in Code Analyser exists but is heavyweight, requires Java, and produces output that’s difficult to integrate into a CI pipeline without post-processing. I wanted something simpler: a single Python command that scans staged Apex files and fails the build when it finds something dangerous.

apex-security-scanner is the result. It maps every finding to an OWASP Top 10 category, outputs clean terminal or JSON results, and runs in under a second on a typical feature branch.

What It Detects

The scanner covers six vulnerability categories drawn from OWASP Top 10 and the Salesforce-specific extensions to it:

SOQL injection - dynamic SOQL built with string concatenation using user-controlled input. The classic pattern is Database.query('SELECT Id FROM Account WHERE Name = \'' + userInput + '\''). The scanner detects variable interpolation in Database.query() and [SELECT ...] dynamic blocks.

Hardcoded credentials - API keys, passwords, and tokens embedded directly in Apex. Common patterns include assignments to variables named apiKey, password, token, secret, and key with string literals. Also flags @RemoteAction methods that expose authentication material.

Missing CRUD/FLS checks - DML operations (insert, update, delete, upsert) that aren’t preceded by a Schema.sObjectType.isCreateable() / isUpdateable() / isDeletable() check. Without these, Apex code ignores the user’s object-level permissions, which is an access control bypass.

Unsafe DML in loops - insert, update, delete inside for loops, which causes governor limit exceptions at scale and is consistently flagged in security reviews.

Dangerous sharing model - classes declared without sharing that perform DML or SOQL. This bypasses Salesforce’s record-level security (sharing rules) entirely.

Open redirects - PageReference or ApexPages.currentPage().getParameters() used with unchecked user input in redirect targets.

Architecture

The scanner is a pure Python CLI with no dependencies beyond the standard library. Each rule is a class inheriting from a base Rule type:

class Rule:
    id: str          # e.g. "A03-SOQL-INJECTION"
    owasp: str       # e.g. "A03:2021 – Injection"
    severity: str    # HIGH | MEDIUM | LOW
    description: str

    def check(self, lines: list[str], filepath: str) -> list[Finding]:
        ...

The scanner walks each .cls file line by line and runs every enabled rule. Findings collect into a list and are rendered either as terminal output (with colour via ANSI codes) or as JSON for CI consumption.

apex-security-scanner/
├── apex_security_scanner/
│   ├── rules/
│   │   ├── soql_injection.py
│   │   ├── hardcoded_credentials.py
│   │   ├── crud_fls.py
│   │   ├── dml_in_loop.py
│   │   ├── sharing_model.py
│   │   └── open_redirect.py
│   ├── scanner.py      # walks files, runs rules, aggregates findings
│   ├── reporter.py     # terminal + JSON output
│   └── cli.py          # argparse entry point
└── pyproject.toml

OWASP Mapping

Every finding includes its OWASP Top 10 category. This isn’t just labelling - it makes the output actionable in organisations that use OWASP as a risk framework:

RuleOWASP Category
SOQL InjectionA03:2021 – Injection
Hardcoded CredentialsA02:2021 – Cryptographic Failures
Missing CRUD/FLSA01:2021 – Broken Access Control
Unsafe DML in LoopsA04:2021 – Insecure Design
Dangerous Sharing ModelA01:2021 – Broken Access Control
Open RedirectA01:2021 – Broken Access Control

Running It

pip install apex-security-scanner

# Scan a directory
apex-scan ./force-app/main/default/classes/

# Scan specific files
apex-scan AccountTriggerHandler.cls OpportunityService.cls

# JSON output for CI
apex-scan ./force-app --format json --output findings.json

# Fail on HIGH severity only
apex-scan ./force-app --min-severity HIGH

CI Integration

The tool is designed to run in a pre-commit hook or a GitHub Actions step. Exit code 0 means clean; non-zero means findings at or above the configured minimum severity.

In GitHub Actions:

- name: Apex security scan
  run: |
    pip install apex-security-scanner
    apex-scan force-app/main/default/classes/ --min-severity MEDIUM

In a pre-commit hook (using the .githooks/pre-commit pattern):

staged=$(git diff --cached --name-only --diff-filter=ACM | grep '\.cls$')
if [ -n "$staged" ]; then
  apex-scan $staged --min-severity HIGH
fi

Limitations and What’s Next

The current scanner is AST-free - it uses regex and line-level pattern matching rather than a full Apex parser. This means:

  • Some patterns require lookahead (e.g. checking if a CRUD check exists anywhere in the same method)
  • It produces false positives on complex multi-line expressions
  • It won’t detect indirect injection through method calls that pass user data through layers

A proper Apex AST parser would solve these at the cost of significantly more complexity. The current approach works well as a first-pass CI gate - it catches the most common issues fast, and the false positive rate is low enough on real codebases to be useful without constant tuning.

The project is open source on GitHub.

← All posts