Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions change/@microsoft-fast-element-5444-csp-style-nonce.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add ElementStyles.styleNonce (aliased as FASTElement.styleNonce) to apply a Content Security Policy nonce to injected style elements.",
"packageName": "@microsoft/fast-element",
"email": "144495202+AKnassa@users.noreply.github.com",
"dependentChangeType": "none"
}
9 changes: 9 additions & 0 deletions packages/fast-element/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,15 @@ CSS during template composition, but runtime CSS bindings and style-attached
through `ElementController.addStyles()` / `removeStyles()`; `ElementStyles`
itself is a static container.

When styles are applied through a style-element strategy, each created
`<style>` node is stamped with the nonce from `ElementStyles.styleNonce`
(aliased as `FASTElement.styleNonce`), allowing those styles to load under a
strict Content Security Policy `style-src` that omits `'unsafe-inline'`. The
nonce is read when styles are applied β€” not when the `ElementStyles` is
constructed β€” so an application can set it at startup even though module-scope
`css` templates have already been evaluated. Adopted stylesheets are not
subject to `style-src` and are unaffected.

---

### Dependency Injection (DI)
Expand Down
24 changes: 24 additions & 0 deletions packages/fast-element/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,27 @@ class MyComponent extends FASTElement {
MyComponent.define({ name: "my-component", template, styles }, [logger()]);
```
Each extension receives the full `FASTElementDefinition`, which includes the resolved element name, type, template, styles, and attribute metadata. Extensions run before `customElements.define()`, so any setup they perform is available when existing DOM elements are upgraded.

## Security

### Content Security Policy

Where the browser supports [adopted stylesheets](https://developer.mozilla.org/docs/Web/API/Document/adoptedStyleSheets), FAST Element applies component styles by adopting constructed stylesheets. Those are not inline styles, so they are not subject to the `style-src` directive and need no additional configuration.

When adopted stylesheets are unavailable, styles are applied by injecting `<style>` elements instead. Under a strict Content Security Policy β€” a `style-src` without `'unsafe-inline'` β€” those elements are blocked unless they carry the policy's nonce. Set `ElementStyles.styleNonce` to the nonce your server emits and it will be applied to every `<style>` element FAST Element creates:

```ts
import { ElementStyles } from "@microsoft/fast-element";

ElementStyles.styleNonce = "{nonce}";
```

`FASTElement.styleNonce` is an alias of the same value, so either may be used:

```ts
import { FASTElement } from "@microsoft/fast-element";

FASTElement.styleNonce = "{nonce}";
```

The nonce is read each time styles are applied rather than when they are created. Because `css` tagged templates are usually evaluated at module scope, the nonce can be configured after those styles exist β€” it only needs to be set before the elements using them are connected.
12 changes: 6 additions & 6 deletions packages/fast-element/SIZES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Bundle sizes for `@microsoft/fast-element` exports.

| Export | Minified | Gzip | Brotli |
|--------|----------|------|--------|
| CDN Rollup Bundle | 78.61 KB | 23.40 KB | 20.77 KB |
| FASTElement (@microsoft/fast-element/fast-element.js) | 23.11 KB | 7.12 KB | 6.41 KB |
| CDN Rollup Bundle | 78.86 KB | 23.48 KB | 20.84 KB |
| FASTElement (@microsoft/fast-element/fast-element.js) | 23.34 KB | 7.23 KB | 6.49 KB |
| Updates (@microsoft/fast-element/updates.js) | 473 B | 335 B | 290 B |
| Observable (@microsoft/fast-element/observable.js) | 6.75 KB | 2.51 KB | 2.23 KB |
| observable (@microsoft/fast-element/observable.js) | 6.79 KB | 2.52 KB | 2.24 KB |
Expand All @@ -17,9 +17,9 @@ Bundle sizes for `@microsoft/fast-element` exports.
| when (@microsoft/fast-element/when.js) | 1.88 KB | 731 B | 589 B |
| html (@microsoft/fast-element/html.js) | 27.73 KB | 8.96 KB | 8.02 KB |
| repeat (@microsoft/fast-element/repeat.js) | 31.80 KB | 10.00 KB | 9.03 KB |
| css (@microsoft/fast-element/css.js) | 2.43 KB | 1.00 KB | 911 B |
| enableHydration (@microsoft/fast-element/hydration.js) | 46.53 KB | 13.91 KB | 12.49 KB |
| css (@microsoft/fast-element/css.js) | 2.53 KB | 1.05 KB | 951 B |
| enableHydration (@microsoft/fast-element/hydration.js) | 46.68 KB | 13.98 KB | 12.55 KB |
| declarativeTemplate (@microsoft/fast-element/declarative.js) | 62.15 KB | 19.46 KB | 17.42 KB |
| ArrayObserver (@microsoft/fast-element/arrays.js) | 12.55 KB | 4.46 KB | 4.03 KB |
| observerMap (@microsoft/fast-element/observer-map.js) | 21.96 KB | 7.73 KB | 6.97 KB |
| attributeMap (@microsoft/fast-element/attribute-map.js) | 15.31 KB | 5.41 KB | 4.88 KB |
| observerMap (@microsoft/fast-element/observer-map.js) | 22.07 KB | 7.77 KB | 7.00 KB |
| attributeMap (@microsoft/fast-element/attribute-map.js) | 15.42 KB | 5.46 KB | 4.91 KB |
3 changes: 3 additions & 0 deletions packages/fast-element/docs/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ export class ElementStyles {
removeStylesFrom(target: StyleTarget): void;
static setDefaultStrategy(Strategy: ConstructibleStyleStrategy): void;
get strategy(): StyleStrategy;
static get styleNonce(): string | null;
static set styleNonce(value: string | null);
// (undocumented)
readonly styles: ReadonlyArray<ComposableStyles>;
static readonly supportsAdoptedStyleSheets: boolean;
Expand Down Expand Up @@ -543,6 +545,7 @@ export interface FASTElementConstructor {
from<TBase extends typeof HTMLElement>(BaseType: TBase): {
new (): InstanceType<TBase> & FASTElement;
};
styleNonce: string | null;
}

// @public
Expand Down
2 changes: 2 additions & 0 deletions packages/fast-element/docs/declarative/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ export class ElementStyles {
removeStylesFrom(target: StyleTarget): void;
static setDefaultStrategy(Strategy: ConstructibleStyleStrategy): void;
get strategy(): StyleStrategy;
static get styleNonce(): string | null;
static set styleNonce(value: string | null);
// (undocumented)
readonly styles: ReadonlyArray<ComposableStyles>;
static readonly supportsAdoptedStyleSheets: boolean;
Expand Down
10 changes: 10 additions & 0 deletions packages/fast-element/src/components/element-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,11 +1006,21 @@ export class StyleElementStrategy implements StyleStrategy {

const styles = this.styles;
const styleClass = this.styleClass;
// Read the nonce here rather than in the constructor, since styles are
// usually created at module scope, before an app can configure a nonce.
const nonce = ElementStyles.styleNonce;

for (let i = 0; i < styles.length; i++) {
const element = document.createElement("style");
element.innerHTML = styles[i];
element.className = styleClass;

if (nonce) {
// Must be set before the element is appended, since that is when
// the Content Security Policy is evaluated against it.
element.setAttribute("nonce", nonce);
}

target.append(element);
}
}
Expand Down
23 changes: 19 additions & 4 deletions packages/fast-element/src/components/fast-element.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type Constructable, isFunction } from "../interfaces.js";
import { ElementStyles } from "../styles/element-styles.js";
import { ElementController } from "./element-controller.js";
import {
applyFASTElementExtensions,
Expand Down Expand Up @@ -196,16 +197,21 @@ export interface FASTElementConstructor {
from<TBase extends typeof HTMLElement>(
BaseType: TBase,
): { new (): InstanceType<TBase> & FASTElement };

/**
* The nonce to apply to style elements created when styles are applied with
* a style element based strategy. An alias of {@link ElementStyles.styleNonce}.
*/
styleNonce: string | null;
}

/**
* A minimal base class for FASTElements that also provides
* static helpers for working with FASTElements.
* @public
*/
export const FASTElement: FASTElementConstructor = Object.assign(
createFASTElement(HTMLElement),
{
export const FASTElement: FASTElementConstructor = Object.defineProperty(
Object.assign(createFASTElement(HTMLElement), {
/**
* Creates a new FASTElement base class inherited from the
* provided base type.
Expand All @@ -220,8 +226,17 @@ export const FASTElement: FASTElementConstructor = Object.assign(
* that describes the element to define.
*/
define,
}),
// Defined as an accessor that forwards to ElementStyles so that the nonce has
// a single source of truth. Object.assign would copy the value, not the accessor.
"styleNonce",
{
get: (): string | null => ElementStyles.styleNonce,
set: (value: string | null) => {
ElementStyles.styleNonce = value;
},
},
);
) as FASTElementConstructor;

/**
* Decorator: Defines a platform custom element based on `FASTElement`.
Expand Down
31 changes: 31 additions & 0 deletions packages/fast-element/src/styles/element-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type ConstructibleStyleStrategy = {
};

let DefaultStyleStrategy: ConstructibleStyleStrategy | undefined;
let styleNonce: string | null = null;

function reduceStyles(
styles: ReadonlyArray<ComposableStyles>,
Expand Down Expand Up @@ -100,6 +101,30 @@ export class ElementStyles {
DefaultStyleStrategy = Strategy;
}

/**
* The nonce to apply to style elements created when styles are applied
* with a style element based strategy. Set this to the nonce of the page's
* Content Security Policy to allow the styles under a strict `style-src`
* that does not include `'unsafe-inline'`. Styles applied with adopted
* style sheets are not subject to `style-src` and are unaffected.
*
* @remarks
* The nonce is read each time styles are applied, so it can be configured
* after styles have been created. This matters because `css` tagged
* templates are typically evaluated at module scope, long before an
* application has a chance to set the nonce.
*/
public static get styleNonce(): string | null {
return styleNonce;
}

public static set styleNonce(value: string | null) {
// Normalized so that a nonce sourced from the DOM by a JS consumer, eg.
// document.querySelector("meta[name=csp-nonce]")?.content, cannot leave
// undefined behind for the getter to hand back.
styleNonce = value ?? null;
}

/**
* Normalizes a set of composable style options.
* @param styles - The style options to normalize.
Expand Down Expand Up @@ -179,11 +204,17 @@ function createStyleElementStrategy(): ConstructibleStyleStrategy {

addStylesTo(target: StyleTarget): void {
const t = target === (document as any) ? document.body : target;
const nonce = styleNonce;

for (let i = 0; i < this.styles.length; i++) {
const element = document.createElement("style");
element.innerHTML = this.styles[i];
element.className = this.styleClass;

if (nonce) {
element.setAttribute("nonce", nonce);
}

(t as any).append(element);
}
}
Expand Down
Loading