Securely processing sensitive data in the browser

Privacy and security are taken very seriously in Contentsquare: they are at the heart of everything we do!

To help companies improving user experience on their websites, we have to collect a lot of anonymous usage data: clicks, scrolls, pageviews, and so on.

GDPR and similar laws around the globe mandate how and what data we can collect and process.

Basically, everything should be anonymized or encrypted in our databases and any decryption key should only be known by website owners, which are our customers.

Securing access to sensitive data

When is encrypted data needed?

Our customers collect data about visitors on their websites. For example, when you call the customer support of your preferred e-commerce website, they know all the articles you have bought.

In some cases however, for instance when troubleshooting specific parts of their websites, our customers need access to some data that we, at Contentsquare, can’t have access to.

For example, think of when errors occur in a form, our customers might need to collect some information to understand what happened. Part of this data can be potentially sensitive, for instance revealing implementation details.

To provide customers with access to this data within Contentsquare, we need to store it securely, and present it only to authorized users, based on specific requests.

How can sensitive data be persisted?

To be able to store sensitive data securely, while preventing Contentsquare to access it, customers can store that information encrypted in a way that only them know the secret to decrypt it.

For that, our customers can generate a pair of keys:

  • A public key, used to encrypt the sensitive data before send it to Contentsquare.
  • A private key, which is the only key that enables to decrypt the data in Contentsquare.

As private keys provide access to sensitive data, we as Contentsquare are neither allowed to persist it, nor to send it to our backend. And every time customers need to access the original data, they will have to provide the private key.

Functional Requirements

Implementing the best solution for this use case should meet all of these prerequisites:

Traceability: As this data is sensitive, it is important to implement an audit log to keep track of who have had access to it.

Usability: When a customer needs to expose sensitive data for different contexts, in the same session, it can be very cumbersome to enter the private key every time.

Security: The private key itself is very sensitive data as well. As such, we can not store it or leak it to our backend. And keeping it in the client should be done in a secure way.

Security concerns at the client

The main security concern in this case is Cross-Site Scripting (XSS) vulnerability.

XSS enable attackers to inject client-side scripts into web pages viewed by other end users. Here’s an example of a script with XSS vulnerability:

<!DOCTYPE html>
<html>
<body>
<label for="name">Name:</label>
<input type="text" id="name" />
<button onclick="onClick()">Click</button>
</body>
<script>
function onClick() {
const name = document.getElementById("name");
document.write(name.value)
}
</script>
</html>

In this case, one could enter the following in the <input> field of id name:

Name input field containing JavaScript expression

This will result in an alert message with a value of the key item stored in the browser sessionStorage.

Hackers can take advantage of this kind of vulnerability and find ways to inject code which will send them data stored in the browser storage.

There are of course best practices to minimize this vulnerability, but still, when dealing with very sensitive data we should avoid storing it on the client altogether. Meaning, we should not use any client storage, like sessionStorage, localStorage, indexedDB, or cookies.

Keeping it “in memory” is the most secure way, but it would not withstand some user journeys, including opened new tabs. Also, the lack of proper abstraction would be more prone to developer mistakes.

Securely storing the private keys

The solution we did implement uses Web Workers, specifically SharedWorker.

Web Workers enable to run scripts in background threads within the browser. Shared Workers are accessible by multiple scripts, even if they are being accessed by different windows.

The SharedWorker interface can be accessed from several browsing contexts, that must share the exact same origin. Communication with the worker thread is done by post messaging.

Once our client enters the private key, we send it to the worker and keep it in memory, without any way to recover it.

Setting the key

Using the private key for decryption

From this point, every decryption operation is done inside the worker, through messaging. The callers send encrypted payloads which get decrypted, and receive the original data in return.

Decrypting the data

Conclusion

We like to think that security and usability go hand in hand, keeping end users experience in mind.

Shared Workers enabled us to keep private keys securely to be reused in the session for exposing sensitive data, and enable to use it between different tabs as well.

At any stage of their project, our customers can single-handedly access their most sensitive data on their website, for their own purposes, with no compromise on security and with a more seamless troubleshooting experience.

Since we started writing this article, our product CS Find & Fix provides dedicated analysis for client-side, custom errors. Learn more about it on contentsquare.com.