Partner POV | Data Theft in Salesforce: Manipulating Public Links
In this article
This article was written and contributed by our partner, Varonis.
Varonis Threat Labs uncovered a vulnerability in Salesforce's public link feature that threat actors could exploit to retrieve sensitive data.
By manipulating the API calls sent to the undocumented Salesforce Aura API — combined with SOQL subqueries — hackers could commit a blind SOQL injection attack to retrieve customer information, including PII.
Varonis Threat Labs informed Salesforce of the vulnerability January 4, 2024. In February 2024, Salesforce patched the vulnerability for blind SOQL injection. Given the severity and the potential of this exploit to expose and leak sensitive information, Varonis researchers intentionally waited to release their findings.
The vulnerability we identified applied to virtually any public link generated by Salesforce, making the potential impact widely detrimental. Because of the ubiquitous nature of public sharing links, most — if not all — Salesforce environments would likely have been vulnerable to some level of exposure, which could lead to data theft or leakage.
Varonis recommends that organizations revisit the Salesforce Permission Sets granted to users to limit the creation of public links, remediate them where feasible, and monitor access activity.
In this blog, we'll explain how Salesforce public links work, how we discovered this vulnerability, and how attackers could exploit it to retrieve sensitive data.
What are public links in Salesforce?
Salesforce public links allow you to share files or folders with people inside or outside your organization without creating user accounts for them. Within Salesforce, files shared via public links can also be attached (or connected) to other records such as accounts, contacts, leads, and more.
How do links work?
When you create a public link for a file, Salesforce generates a URL that can be shared with anyone inside or outside the organization. However, the URL is not a direct link to the file.
Instead, the URL leads to a small Salesforce Lightning application, which will verify a password (if necessary), retrieve the file, and, in some cases, show the file in the browser preview.
Salesforce Lightning uses Aura components for front-end elements. Those components send requests to Aura endpoints to perform server-side actions such as data retrieval. In effect, when a public link is created, a new Aura endpoint — accessible to unauthenticated users — is created. Users can communicate directly with these endpoints using the undocumented Aura API as unauthenticated users.
People who click on public links from Salesforce are a special inaccessible "hidden external user." This user has a restricted set of permissions required to access the file. An admin cannot control or modify the permissions of the "hidden external user" because it's hidden and inaccessible.
How do public links request information?
When a user clicks on a Salesforce public link, the Lightning app requests information about the public link, using the following method and parameters:
serviceComponent://ui.content.components.forceContent.contentDistributionViewer. ContentDistributionViewerController /ACTION$getContentDistributionInfo
There are three parts to a method.
- Namespace: This prefix determines the location or package of the controller whose method is being called. In our case, the namespace is "ui.content.components.forceContent.contentDistributionViewer".
- Controller class: This is the name of the controller or Apex class that contains the method. Here the controller class is ContentDistributionViewerController.
- Action: This is the name of the specific method we want to call. Here, the method is getContentDistributionInfo.
The following parameters are included with the request:
- The ID of the link record: This is automatically received earlier in the JavaScript code when the Lightning app is loaded. However, the link record ID can also be directly inferred from the link itself.
- IsInternalView: This is an empty string.
- dpt: This value is required if the link is protected using a password. If there's no password, an empty string is provided.
This method will return the IDs of the specific file (ContentDocument) and file version (ContentVersion) shared using the public link, along with more information, such as the file type, version number, whether a preview is available, and more.
Abusing the Aura endpoint and API
Having established that public links create Aura endpoints, we sought to find ways to exploit that access.
We covered Aura exploits before in our research on abusing Salesforce communities and ghost sites.
We tried abusing the Aura endpoint behind a public link to access more data from the Salesforce environment, including data of records associated with the link.
We started with the most basic Aura method: getting the config data. Surprisingly, the getConfigData method which usually returns some information, returned an "Unable to Process Request" error.
We tried other Aura methods but received the same error. We revised our methods and checked the encoding multiple times, attempting to locate the origin of the error, until a researcher noticed our query parameters and method did not match. Changing the query parameters proved to be the breakthrough needed.
What are query parameters?
Query parameters provide information to web servers when making requests.
In typical scenarios, like a user navigating a Lightning interface through a web browser, Salesforce communicates to the server by using query parameters to indicate the methods included in the request.
Usually, Aura endpoints are not affected if query parameters and methods do not match. However, given the errors received, we sought to test if forcing the methods and query parameters to match would work.
In Salesforce Aura, query parameters are based on the method used, with three parts separated by a dot(.), and a numeric value such as 1. The name of the query parameters initially provided is:
/ui-content-components-forceContent-contentDistributionViewer. ContentDistributionViewer.getContentDistributionInfo=1
The query parameter has the same three parts as the method above, but with a different formatting. The are three parts to the query parameter.
- Namespace: This prefix helps to define what method is being called and changes depending on the method used. For service component methods, the namespace is all the parts that lead to the controller, with a hyphen instead of a dot. So, in our case: "ui-content-components-forceContent-ContentDistributionViewer".
- Controller class: This is the name of the controller or Apex class. When used in a query, the word "Controller" is dropped, thus ContentDistributionViewerController is written as ContentDistributionViewer.
- Action: This is the specific action being called. In this case, we're requesting information about content. When in use, ACTION$, is dropped and will display as getContentDistributionInfo.
We attempted to call getConfigData again, but this time with a new query parameter:
ui-force-components-controllers-hostConfig.HostConfig.getConfigData
This produced a successful response.
Next, we tried listing ContentDocument records. This produced an error message.
We concluded that there are two reasons why an action could be blocked:
- The method itself is blocked
- The method is allowed, but not with the provided parameters
To continue the research, we needed to distinguish between the two potential causes for an action to be blocked.
We devised a test to determine which methods were valid. By specifying a query parameter (which typically matches the method used) but keeping the actions list empty, there is only one variable being tested — the method itself.
If a method is valid, then submitting a query parameter with an empty action list should return an Aura response with no actions. We sent a request without actions, and as expected we received an Aura response:
But when we tried submitting query parameters with empty actions using a forbidden method, we received an error:
With this test, we can use the query parameters to determine whether the method itself is forbidden, or if the problem is the parameters.
To quickly test all the combinations, we used Burp Intruder, a Burp Suite tool that lets users send many requests simultaneously and observe the response.
We created and tested a series of payloads. Creating our test payloads required us to assemble and correctly format a list of almost 500 Aura methods, that we at Varonis Threat Labs uncovered during our deep dive into Salesforce security and potential threat vectors.
We ended up with a very short list of allowed methods:
One method that stood out is getRecord, specifically:
serviceComponent:// ui.force.components.controllers.recordGlobalValueProvider.RecordGvpController /ACTION$getRecord
The method getRecord is very powerful. It allows a user to specify the fields they want to retrieve, including related entities. The getRecord method works using SOQL and it builds the query using the provided fields.
We can use those fields to inject subqueries to retrieve more data but cannot use the fields to see the results of the subquery, because that method does not support subqueries. Instead, any response the subquery receives is displayed as an error message, forcing us to make a blind attack.
SOQL subquery blind attack
Basic SOQL queries look a lot like SQL queries, but they are not the same. One key difference is how their table relationships work. In SQL, the JOIN clause is used to query multiple tables simultaneously based on a shared value(s), but SOQL does not support JOIN. Instead, SOQL uses a subquery.
For example, files — or ContentDocument records — have related identities. One of them is the owner, but files can also be attached to other records such as accounts, contracts, and more. Files have a many-to-many relationship and a table called ContentDocumentLink handles those relationships. If we wanted the name of a user attached to a ContentDocument in SQL, the query would look something like this:
SELECT ContentDocument.ID, User.Name FROM ContentDocument JOIN ContentDocumentLink ON ContentDocumentLink.ContentDocumentID = ContentDocument.ID JOIN User ON User.ID = ContentDocumentLink.LinkedEntityID
But this is not SQL; it's SOQL. So instead, the subquery would be built like this:
SELECT ID, (SELECT LinkedEntity.Name FROM ContentDocumentLinks WHERE LinkedEntity.Type = 'User') FROM ContentDocument
In this example, ContentDocumentLinks is the name of the relationship between ContentDocumentLink and ContentDocument. In fact, there are two types of subqueries — one in SELECT and one in WHERE. The main difference is the WHERE subqueries query tables whereas SELECT subqueries query relationships. This difference is important when abusing SOQL-based vulnerabilities.
After misconfigurations, SELECT and WHERE subquery SOQL injections make up the most common attack vectors used to abuse Salesforce-based apps.
In our case, we can insert a SELECT subquery. SELECT subqueries are a powerful tool, but our use case is quite simple. Let's see how a subquery might let us retrieve data that's typically restricted.
As mentioned before, calling a subquery directly leads to an internal error:
But we only get an error if the subquery returns results. So, we can use an inner WHERE inside the SELECT subquery. For example, we can use LIKE:
SELECT Id, (SELECT LinkedEntity.Name FROM ContentDocumentLinks WHERE LinkedEntity.Name LIKE 'A%') FROM ContentDocument
For example, if there is a linked entity with a name starting with "A," our subquery will yield a result and produce an error message. If there is no linked entity with a name starting with "A," our subquery will yield no results and, consequently, produce no error message.
By repeating the subquery process, character by character, and specifying different fields, we deduced entire names, email addresses, and phone numbers. If the ContentDocument is attached to an account, lead, or contact, we can gain information about customers as well. To save time and manual effort, we created and ran a small script:
This resulted in us learning the phone number and the file owner's name. In other cases, we managed to deduce additional sensitive information and PII, including phone numbers and email addresses from accounts, leads, users, and other records.
Reduce the blast radius.
The most efficient way of reducing your blast radius is to remove Salesforce public links whenever possible.
Varonis allows you to identify and remove the ability to create public links from users who don't need those permissions, as well as remove existing links that expose sensitive information — all without navigating complex Salesforce Profiles or Permission Sets.
Learn more about Data Protection and Varonis Contact a WWT Expert