Skip to content

Add support for CSSStyleSheet instance in DomRenderer for CSP enabled applications without relying on nonce #1666

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
42 changes: 42 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,48 @@ jss.setup({
})
```

## Using a `CSSStyleSheet` instance for secure style injection

For environments with strict Content Security Policy (CSP) settings, JSS now supports injecting styles into a `CSSStyleSheet` instance. This approach is particularly useful when inline styles are restricted and a nonce value is unavailable or not exposed.

### Example

Create a `CSSStyleSheet` instance and pass it to JSS during setup:

```javascript
import jss from 'jss'

const sheet = new CSSStyleSheet()
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]

jss.setup({
insertionPoint: sheet // Pass the CSSStyleSheet instance
})

// Create your style.
const style = {
myButton: {
color: 'green'
}
}

// Compile styles, apply plugins.
const jssSheet = jss.createStyleSheet(style)

// Inject styles directly into the provided CSSStyleSheet.
jssSheet.attach()
```

### Benefits

- Enables secure style injection into a `CSSStyleSheet` instance, avoiding inline styles.
- Works seamlessly in CSP-enabled applications where the nonce attribute cannot be used or accessed.

### Notes

- This feature is an alternative to nonce-based CSP compliance. Both approaches are supported.
- Ensure the provided `CSSStyleSheet` instance is valid and adopted by the document where styles need to be applied.

## Configuring Content Security Policy

You might need to set the `style-src` CSP directive, but do not want to set it to `unsafe-inline`. See [these instructions for configuring CSP](csp.md).
Expand Down
47 changes: 35 additions & 12 deletions packages/jss/src/DomRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,16 @@ function insertStyle(style, options) {
return
}

// if insertionPoint is instance of CSSStyleSheet then we can insert the rule at the end of the stylesheet
// and we don't need to create a new style element
if (insertionPoint instanceof CSSStyleSheet) {
try {
insertionPoint.insertRule(style.textContent, insertionPoint.cssRules.length)
} catch (err) {
warning(false, `[JSS] ${err.message}`)
}
return
}
getHead().appendChild(style)
}

Expand Down Expand Up @@ -276,13 +286,19 @@ export default class DomRenderer {
if (sheet) sheets.add(sheet)

this.sheet = sheet
const {media, meta, element} = this.sheet ? this.sheet.options : {}
this.element = element || createStyle()
this.element.setAttribute('data-jss', '')
if (media) this.element.setAttribute('media', media)
if (meta) this.element.setAttribute('data-meta', meta)
const nonce = getNonce()
if (nonce) this.element.setAttribute('nonce', nonce)
const {media, meta, element, insertionPoint} = this.sheet ? this.sheet.options : {}
if (insertionPoint instanceof CSSStyleSheet) {
// If insertionPoint is an instance of CSSStyleSheet, use it directly.
this.element = insertionPoint
} else {
// Otherwise, create a new style element.
this.element = element || createStyle()
this.element.setAttribute('data-jss', '')
if (media) this.element.setAttribute('media', media)
if (meta) this.element.setAttribute('data-meta', meta)
const nonce = getNonce()
if (nonce) this.element.setAttribute('nonce', nonce)
}
}

/**
Expand Down Expand Up @@ -345,14 +361,21 @@ export default class DomRenderer {
* Insert a rule into element.
*/
insertRule(rule, index, nativeParent = this.element.sheet) {
// create a new variable to hold the nativeParent
let parentNode = nativeParent
// If the element is an instance of CSSStyleSheet, use it directly.
if (this.element instanceof CSSStyleSheet) {
parentNode = this.element
}

if (rule.rules) {
const parent = rule
let latestNativeParent = nativeParent
let latestNativeParent = parentNode
if (rule.type === 'conditional' || rule.type === 'keyframes') {
const insertionIndex = getValidRuleInsertionIndex(nativeParent, index)
const insertionIndex = getValidRuleInsertionIndex(parentNode, index)
// We need to render the container without children first.
latestNativeParent = insertRule(
nativeParent,
parentNode,
parent.toString({children: false}),
insertionIndex
)
Expand All @@ -369,8 +392,8 @@ export default class DomRenderer {

if (!ruleStr) return false

const insertionIndex = getValidRuleInsertionIndex(nativeParent, index)
const nativeRule = insertRule(nativeParent, ruleStr, insertionIndex)
const insertionIndex = getValidRuleInsertionIndex(parentNode, index)
const nativeRule = insertRule(parentNode, ruleStr, insertionIndex)
if (nativeRule === false) {
return false
}
Expand Down