How to make style element in Shadow DOM work with strict CSP?

Viewed 603

In recent years, the Web Components standard has emerged as the standard low-level GUI API for the web. One of its key features is the Shadow DOM, which encapsulates DOM, enabling greatly simplified CSS selectors.

an elementary example might be,

const template = document.createElement('template');
template.innerHTML = /*html*/`
  <style>
    p {
      color: red;
    }
  </style>
  <p>Hello, world!</p>
`;

class HelloWorld extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

customElements.define('hello-world', HelloWorld);

in my HTML I would call my component like so,

<!DOCTYPE html>
<html>

<head>
  <script type="module" src="hello-world.js" defer></script>
</head>

<body>
  <hello-world></hello-world>
</body>

</html>

This approach is being cemented as the industry baseline, with frameworks like Lit, React, Angular, Vue, etc., on top. Now let us say I want to apply a strict CSP.

To do so, I start with the following policy,

Content-Security-Policy "default-src 'self';"

But it does not work because the style element in the web component definition is inline. The only solutions I am aware of are to add either a nonce, a hash, or unsafe-inline. I will not consider unsafe-inline since it detracts security which negates the point of the question, which is to maintain a strict CSP. Let us consider the other two alternatives.

This is where I start making conjectures, so correct me if I am wrong. Making either a hash or a nonce is out of the question for all but the simplest of situations. Only completely static content could work with hashes. Once the content becomes dynamic, the hash becomes wrong, and the CSP will block the inline style. And that is beside the point of creating a hash for the HTML embedded inside the js file that defines the web component. A nonce poses a different, equally as difficult issue. Say I generate a nonce and include it in my CSP header. How do I inject it into the js file?

Besides making an addition to the CSP spec to allow inline styles added by allowed scripts, I can not imagine a reasonable solution.

How can this be done in any way at all?

1 Answers

In 2017 the advice was to use the <link> element

Here are the lead developers from Apple (rniwa), Google (hayatoito) and Mozilla (annevk) commenting:

An issue from 2017... maybe open it or create a new one.

BTW, (not nonce related)
attaching a shadowRoot in the connectedCallback will cause an error when moving DOM elements, because a shadowRoot can only be added once (and not removed)

Everything can be chained in the constructor:

  constructor() {
    super() // sets and returns 'this'
       .attachShadow({ mode: 'open' })  // sets and returns this.shadowRoot
       .append(template.content.cloneNode(true)); // no need for appendChild
  }
Related