Skip to content

Commit

Permalink
[TASK] Add CSP nonce helper for inline styles in lit-element templates
Browse files Browse the repository at this point in the history
When using Content-Security-Policy for `style-src` with a `nonce-...`
value, it requires that inline styles (those using a `<style>` element)
have to be granted with a corresponding `nonce="..."` attribute.

Note: 'unsafe-inline' is ignored when using a nonce or hashes.
This behavior is decribed in CSP L3 in section 6.7.3.2.:2.1
(https://w3c.github.io/webappsec-csp/#allow-all-inline)
> If expression matches the nonce-source or hash-source grammar,
> return "Does Not Allow".

Even if `<style>` usages in lit-element templates are static in most
cases, it is considered a "inline style" in the scope of CSP.

This change introduces a work-around, exposing `window.litNonce`
in the global JavaScript context. In case a malicous script manages
to retrieve this information, it does not really matter, since the
malicious script was already executed with a valid nonce before...

Resolves: #100140
Releases: main
Change-Id: I53c2967f2c80c0f862145a4c94d75a5fc1349205
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/78231
Tested-by: core-ci <[email protected]>
Reviewed-by: Benni Mack <[email protected]>
Tested-by: Benni Mack <[email protected]>
Tested-by: Andreas Fernandez <[email protected]>
Reviewed-by: Andreas Fernandez <[email protected]>
  • Loading branch information
ohader authored and andreaskienast committed Mar 24, 2023
1 parent b599199 commit dfa794f
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import { html, LitElement, TemplateResult, render } from 'lit';
import { customElement, property } from 'lit/decorators';
import { lll } from '@typo3/core/lit-helper';
import { lll, styleTag } from '@typo3/core/lit-helper';
import '@typo3/backend/element/icon-element';
import Severity from '@typo3/backend/severity';
import Modal from '@typo3/backend/modal';
Expand Down Expand Up @@ -172,9 +172,9 @@ export class TableWizardElement extends LitElement {
const lastRowIndex = this.table.length - 1;

return html`
<style>
${styleTag`
:host, typo3-backend-table-wizard { display: inline-block; }
</style>
`}
<div class="table-fit table-fit-inline-block">
<table class="table table-center">
<thead>
Expand Down
35 changes: 34 additions & 1 deletion Build/Sources/TypeScript/core/lit-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@
* The TYPO3 project - inspiring people to share!
*/

import { render, TemplateResult } from 'lit';
import { html, HTMLTemplateResult, render, TemplateResult } from 'lit';
import { ClassInfo } from 'lit/directives/class-map';

interface LitNonceWindow extends Window {
litNonce?: string;
}

/**
* @internal
*/
Expand Down Expand Up @@ -53,3 +57,32 @@ export const classesArrayToClassInfo = (classes: Array<string>): ClassInfo => {
{} as Writeable<ClassInfo>
);
};

/**
* Creates style tag having using a nonce-value if declared in `window.litNonce`
* (see https://lit.dev/docs/api/ReactiveElement/#ReactiveElement.styles)
*
* @example
* ```
* return html`
* ${styleTag`
* .my-style { ... }
* `}
* <div class="my-style">...</div>
* `
* ```
* produces a template result containing
* ```
* <style nonce="...">
* .my-style { ... }
* </style>
* <div class="my-style">...</div>
* ```
*/
export const styleTag = (strings: TemplateStringsArray): HTMLTemplateResult => {
const nonce = (window as LitNonceWindow).litNonce;
if (nonce) {
return html`<style nonce="${nonce}">${strings}</style>`;
}
return html`<style>${strings}</style>`;
};
6 changes: 6 additions & 0 deletions Build/ckeditor5.rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import resolve from '@rollup/plugin-node-resolve';
import postcss from 'rollup-plugin-postcss';
import svg from 'rollup-plugin-svg';
import * as path from 'path';

const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );
const postCssConfig = styles.getPostCssConfig({
Expand All @@ -23,6 +24,11 @@ export default [{
plugins: [
postcss({
...postCssConfig,
inject: function(cssVariableName, fileId) {
// overrides functionality of native `style-inject` package, now applies `window.litNonce` to `<style>`
const importPath = path.resolve('./ckeditor5.rollup.functions.js');
return `import styleInject from '${importPath}';\n` + `styleInject(${cssVariableName});`;
},
// @todo unsure whether we might give up a stand alone style file
// style information is bundled inside the JavaScript bundle, that's how it's documented as well
// extract: path.resolve('../typo3/sysext/rte_ckeditor/Resources/Public/Contrib/ckeditor5.css')
Expand Down
24 changes: 24 additions & 0 deletions Build/ckeditor5.rollup.functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* duplicated from https://github.com/egoist/style-inject/blob/04ca45c34f20f0aa63d3d68e668de037d24579ad/src/index.js
* extended by nonce capabilities
*/
export default function styleInject(css, { insertAt } = {}) {
if (!css || typeof document === 'undefined') return

const head = document.head || document.getElementsByTagName('head')[0]
const style = document.createElement('style')
style.type = 'text/css'
if (window['litNonce']) {
style.setAttribute('nonce', window['litNonce']);
}
if (insertAt === 'top' && head.firstChild) {
head.insertBefore(style, head.firstChild)
} else {
head.appendChild(style)
}
if (style.styleSheet) {
style.styleSheet.cssText = css
} else {
style.appendChild(document.createTextNode(css))
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __decorate=function(t,e,l,a){var o,n=arguments.length,i=n<3?e:null===a?a=Object.getOwnPropertyDescriptor(e,l):a;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(t,e,l,a);else for(var s=t.length-1;s>=0;s--)(o=t[s])&&(i=(n<3?o(i):n>3?o(e,l,i):o(e,l))||i);return n>3&&i&&Object.defineProperty(e,l,i),i};import{html,LitElement,render}from"lit";import{customElement,property}from"lit/decorators.js";import{lll}from"@typo3/core/lit-helper.js";import"@typo3/backend/element/icon-element.js";import Severity from"@typo3/backend/severity.js";import Modal from"@typo3/backend/modal.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";let TableWizardElement=class extends LitElement{constructor(){super(...arguments),this.type="textarea",this.selectorData="",this.delimiter="|",this.enclosure="",this.appendRows=1,this.table=[]}get firstRow(){return this.table[0]||[]}connectedCallback(){super.connectedCallback(),this.selectorData=this.getAttribute("selector"),this.delimiter=this.getAttribute("delimiter"),this.enclosure=this.getAttribute("enclosure")||"",this.readTableFromTextarea()}createRenderRoot(){return this}render(){return this.renderTemplate()}provideMinimalTable(){0!==this.table.length&&0!==this.firstRow.length||(this.table=[[""]])}readTableFromTextarea(){const t=document.querySelector(this.selectorData),e=[];t.value.split("\n").forEach((t=>{if(""!==t){this.enclosure&&(t=t.replace(new RegExp(this.enclosure,"g"),""));const l=t.split(this.delimiter);e.push(l)}})),this.table=e}writeTableSyntaxToTextarea(){const t=document.querySelector(this.selectorData);let e="";this.table.forEach((t=>{const l=t.length;e+=t.reduce(((t,e,a)=>{const o=l-1===a?"":this.delimiter;return t+this.enclosure+e+this.enclosure+o}),"")+"\n"})),t.value=e,t.dispatchEvent(new CustomEvent("change",{bubbles:!0}))}modifyTable(t,e,l){const a=t.target;this.table[e][l]=a.value,this.writeTableSyntaxToTextarea(),this.requestUpdate()}toggleType(){this.type="input"===this.type?"textarea":"input"}moveColumn(t,e){this.table=this.table.map((l=>{const a=l.splice(t,1);return l.splice(e,0,...a),l})),this.writeTableSyntaxToTextarea(),this.requestUpdate()}appendColumn(t,e){this.table=this.table.map((t=>(t.splice(e+1,0,""),t))),this.writeTableSyntaxToTextarea(),this.requestUpdate()}removeColumn(t,e){this.table=this.table.map((t=>(t.splice(e,1),t))),this.writeTableSyntaxToTextarea(),this.requestUpdate()}moveRow(t,e,l){const a=this.table.splice(e,1);this.table.splice(l,0,...a),this.writeTableSyntaxToTextarea(),this.requestUpdate()}appendRow(t,e){const l=this.firstRow.concat().fill(""),a=new Array(this.appendRows).fill(l);this.table.splice(e+1,0,...a),this.writeTableSyntaxToTextarea(),this.requestUpdate()}removeRow(t,e){this.table.splice(e,1),this.writeTableSyntaxToTextarea(),this.requestUpdate()}renderTemplate(){this.provideMinimalTable();const t=Object.keys(this.firstRow).map((t=>parseInt(t,10))),e=t[t.length-1],l=this.table.length-1;return html`
<style>
var __decorate=function(t,e,l,a){var o,n=arguments.length,i=n<3?e:null===a?a=Object.getOwnPropertyDescriptor(e,l):a;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(t,e,l,a);else for(var s=t.length-1;s>=0;s--)(o=t[s])&&(i=(n<3?o(i):n>3?o(e,l,i):o(e,l))||i);return n>3&&i&&Object.defineProperty(e,l,i),i};import{html,LitElement,render}from"lit";import{customElement,property}from"lit/decorators.js";import{lll,styleTag}from"@typo3/core/lit-helper.js";import"@typo3/backend/element/icon-element.js";import Severity from"@typo3/backend/severity.js";import Modal from"@typo3/backend/modal.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";let TableWizardElement=class extends LitElement{constructor(){super(...arguments),this.type="textarea",this.selectorData="",this.delimiter="|",this.enclosure="",this.appendRows=1,this.table=[]}get firstRow(){return this.table[0]||[]}connectedCallback(){super.connectedCallback(),this.selectorData=this.getAttribute("selector"),this.delimiter=this.getAttribute("delimiter"),this.enclosure=this.getAttribute("enclosure")||"",this.readTableFromTextarea()}createRenderRoot(){return this}render(){return this.renderTemplate()}provideMinimalTable(){0!==this.table.length&&0!==this.firstRow.length||(this.table=[[""]])}readTableFromTextarea(){const t=document.querySelector(this.selectorData),e=[];t.value.split("\n").forEach((t=>{if(""!==t){this.enclosure&&(t=t.replace(new RegExp(this.enclosure,"g"),""));const l=t.split(this.delimiter);e.push(l)}})),this.table=e}writeTableSyntaxToTextarea(){const t=document.querySelector(this.selectorData);let e="";this.table.forEach((t=>{const l=t.length;e+=t.reduce(((t,e,a)=>{const o=l-1===a?"":this.delimiter;return t+this.enclosure+e+this.enclosure+o}),"")+"\n"})),t.value=e,t.dispatchEvent(new CustomEvent("change",{bubbles:!0}))}modifyTable(t,e,l){const a=t.target;this.table[e][l]=a.value,this.writeTableSyntaxToTextarea(),this.requestUpdate()}toggleType(){this.type="input"===this.type?"textarea":"input"}moveColumn(t,e){this.table=this.table.map((l=>{const a=l.splice(t,1);return l.splice(e,0,...a),l})),this.writeTableSyntaxToTextarea(),this.requestUpdate()}appendColumn(t,e){this.table=this.table.map((t=>(t.splice(e+1,0,""),t))),this.writeTableSyntaxToTextarea(),this.requestUpdate()}removeColumn(t,e){this.table=this.table.map((t=>(t.splice(e,1),t))),this.writeTableSyntaxToTextarea(),this.requestUpdate()}moveRow(t,e,l){const a=this.table.splice(e,1);this.table.splice(l,0,...a),this.writeTableSyntaxToTextarea(),this.requestUpdate()}appendRow(t,e){const l=this.firstRow.concat().fill(""),a=new Array(this.appendRows).fill(l);this.table.splice(e+1,0,...a),this.writeTableSyntaxToTextarea(),this.requestUpdate()}removeRow(t,e){this.table.splice(e,1),this.writeTableSyntaxToTextarea(),this.requestUpdate()}renderTemplate(){this.provideMinimalTable();const t=Object.keys(this.firstRow).map((t=>parseInt(t,10))),e=t[t.length-1],l=this.table.length-1;return html`
${styleTag`
:host, typo3-backend-table-wizard { display: inline-block; }
</style>
`}
<div class="table-fit table-fit-inline-block">
<table class="table table-center">
<thead>
Expand Down
10 changes: 10 additions & 0 deletions typo3/sysext/core/Classes/Page/PageRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,15 @@ protected function renderMainJavaScriptLibraries()
{
$out = '';

// adds a nonce hint/work-around for lit-elements (which is only applied automatically in ShadowDOM)
// see https://lit.dev/docs/api/ReactiveElement/#ReactiveElement.styles)
if ($this->nonce instanceof Nonce) {
$out .= GeneralUtility::wrapJS(
sprintf('window.litNonce = %s;', GeneralUtility::quoteJSvalue($this->nonce->b64)),
['nonce' => $this->nonce->b64]
);
}

if (!$this->addRequireJs && $this->javaScriptRenderer->hasRequireJs()) {
$this->loadRequireJs();
}
Expand Down Expand Up @@ -2140,6 +2149,7 @@ protected function renderMainJavaScriptLibraries()
if ($this->getApplicationType() === 'BE') {
$this->javaScriptRenderer->addGlobalAssignment(['TYPO3' => $assignments]);
} else {
// @todo apply nonce for CSP (means dropping static `inlineJavascriptWrap`)
$out .= sprintf(
"%svar TYPO3 = Object.assign(TYPO3 || {}, %s);\r\n%s",
$this->inlineJavascriptWrap[0],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
import{render}from"lit";export const renderNodes=e=>{const n=document.createElement("div");return render(e,n),n.childNodes};export const renderHTML=e=>{const n=document.createElement("div");return render(e,n),n.innerHTML};export const lll=e=>window.TYPO3&&window.TYPO3.lang&&"string"==typeof window.TYPO3.lang[e]?window.TYPO3.lang[e]:"";export const classesArrayToClassInfo=e=>e.reduce(((e,n)=>(e[n]=!0,e)),{});
import{html,render}from"lit";export const renderNodes=e=>{const n=document.createElement("div");return render(e,n),n.childNodes};export const renderHTML=e=>{const n=document.createElement("div");return render(e,n),n.innerHTML};export const lll=e=>window.TYPO3&&window.TYPO3.lang&&"string"==typeof window.TYPO3.lang[e]?window.TYPO3.lang[e]:"";export const classesArrayToClassInfo=e=>e.reduce(((e,n)=>(e[n]=!0,e)),{});export const styleTag=e=>{const n=window.litNonce;return n?html`<style nonce="${n}">${e}</style>`:html`<style>${e}</style>`};
Original file line number Diff line number Diff line change
Expand Up @@ -10939,26 +10939,24 @@ class ContextPlugin extends ObservableMixin() {
static get isContextPlugin() {
return true;
}
}function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;

if (!css || typeof document === 'undefined') { return; }
}/**
* duplicated from https://github.com/egoist/style-inject/blob/04ca45c34f20f0aa63d3d68e668de037d24579ad/src/index.js
* extended by nonce capabilities
*/
function styleInject(css, { insertAt } = {}) {
if (!css || typeof document === 'undefined') return

var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
const head = document.head || document.getElementsByTagName('head')[0];
const style = document.createElement('style');
style.type = 'text/css';

if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
if (window['litNonce']) {
style.setAttribute('nonce', window['litNonce']);
}
if (insertAt === 'top' && head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}

if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
Expand Down

0 comments on commit dfa794f

Please sign in to comment.