Writing a secured web component

Pawel Uchida-Psztyc
4 min readNov 13, 2019

Writing a web component that securely access, process, and renders restricted data can be a challenging task. When you are sharing a web component with others, you may start being concerned about leaking protected information to 3rd party services. This article tackles this problems and suggest some solutions.

The problem

In Salesforce cloud an application, which can be a web component build on top of Lightning Web Components, can work alongside other applications inside a single document. The platform isolates applications from each other so there’s no way to extract the data from a component by a malicious application. But imagine having a component that authenticate a user with your API, fetches data from it, and is working with 3rd party applications outside Salesforce cloud. You have no control over how it is used and how the properties are accessed. Can data received from the API be securely stored inside the component?

Possible option would be to store API data outside of the scope of initialized component in a global (for a module) variable.

let apiData;
class MyElement extends HTMLElement {
async getData() {
const response = await fetch('https://my.api.com');
apiData = await response.json();
}
}

This could only work if the element is to be used only once in the document because next instance of the same component would override the data. In near future we will be able to use private fields in the class definition. However, this specification at the moment is a candidate (API stage 3 proposal) and it’s not widely implemented.

Today we can use WeakMap to store the data in the component’s global scope but private for the module. To add something to a WeakMap the key has to be an object. When this object gets destroyed the garbage collector also destroys the data associated with the object. Hence weak in WeakMap.

When the data is ready put it into the map using this as the key. It is now easily accessible from the instance of the component, secured from unauthorized access, and garbage collected when not needed anymore.

const apiData = new WeakMap();class MyElement extends HTMLElement {
async getData() {
const response = await fetch('https://my.api.com');
const data = await response.json();
apiData.set(this, data);
}
}

What about access to instance methods?

Instance methods, like getData(), is added to component’s prototype and can be accessed by the hosting application. For some components, like ours, it could be unacceptable to expose methods that directly interact with the API. On the other hand this method has to be accessible from the component instance to, for example, you can call an event handler from the view.

To solve this we should get familiar with symbols introduced in ECMAScript 6. An instance of a symbol can be used as the function name in the prototype, instead of normally used string name.

const apiData = new WeakMap();
const getData = Symbol();
class MyElement extends HTMLElement {
connectedCallback() {
this[getData]();
}
async [getData]() {
const response = await fetch('https://my.api.com');
const data = await response.json();
apiData.set(this, data);
}
}
customElements.define('my-element', MyElement);

A feature of symbols is that an instance of a symbol does not equal another instance of a symbol, even if created with the same argument. Because of that property, you cannot invoke getData() method without having a reference to the original symbol. Even when you inspect instance of my-element you won’t see this function.

Rendering the data

The last part is to render the data. You can use your favorite library to do this, say lit-html. Unfortunately, what is rendered inside the component’s shadow DOM is also available by another application. Once you put a text node into the element’s DOM it can be read by other script. This means some script can assume a structure of your data and scrape it from your component. You could possibly leave it like this. The rendered data probably won’t be exactly the same as the one received from the API. A single change to component’s internal DOM structure would break the script. But if it is not good enough for you the solution would be to close the shadow DOM. When attaching shadow DOM to the element you can choose to set mode to “closed”. In this case no one will be able to access the internal DOM of the component. Unfortunately, including yourself. You have to store a reference to the shadowRoot somewhere to access the DOM.

const shadowRoots = new WeakMap();class MyElement extends HTMLElement {
constructor() {
const root = this.attachShadow({ mode: 'closed' });
shadowRoots.set(this, root);
}
}

Now when rendering the view you can access the shadow DOM using this keyword and only our component has access to it.

Summarizing

When designing a web component for security we can completely isolate component’s internal DOM, it’s properties, and methods from the environment. This way we can create a self contained component that can make API calls and render the data securely, without worrying about leaking of data.

--

--

Pawel Uchida-Psztyc

Design, APIs, front-end, strategy, product, and educator. I work for big tech but I share my personal views and opinions.