Skip to content

Commit e620444

Browse files
Steve Orvelldfreedm
Steve Orvell
authored andcommitted
Adds unsafeCss for composing values into css (#474)
* Adds `unsafeCss` for composing values into `css` Fixes #451 and #471. Non-literal values can now be included in styling with `css` by using the `unsafeCss` function. This is named "unsafe" so users know this they must use this carefully to avoid security issues. * Address review feedback * add docs * refine error message
1 parent 33c4794 commit e620444

File tree

3 files changed

+59
-4
lines changed

3 files changed

+59
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1717

1818
### Added
1919
* [Maintenance] Added script to publish dev releases automatically ([#476](https://github.com/Polymer/lit-element/pull/476)).
20+
* Adds `unsafeCss` for composing "unsafe" values into `css`. Note, `CSSResult` is no longer constructable. ([#451](https://github.com/Polymer/lit-element/issues/451) and [#471](https://github.com/Polymer/lit-element/issues/471)).
2021

2122
### Fixed
2223
* Fixed a bug where we broke compatibility with closure compiler's property renaming optimizations. JSCompiler_renameProperty can't be a module export ([#465](https://github.com/Polymer/lit-element/pull/465)).

src/lib/css-tag.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@ found at http://polymer.github.io/PATENTS.txt
1212
export const supportsAdoptingStyleSheets =
1313
('adoptedStyleSheets' in Document.prototype);
1414

15+
const constructionToken = Symbol();
16+
1517
export class CSSResult {
1618

1719
_styleSheet?: CSSStyleSheet|null;
1820

1921
readonly cssText: string;
2022

21-
constructor(cssText: string) { this.cssText = cssText; }
23+
constructor(cssText: string, safeToken: symbol) {
24+
if (safeToken !== constructionToken) {
25+
throw new Error('CSSResult is not constructable. Use `unsafeCss` or `css` instead.');
26+
}
27+
this.cssText = cssText;
28+
}
2229

2330
// Note, this is a getter so that it's lazy. In practice, this means
2431
// stylesheets are not created until the first element instance is made.
@@ -37,20 +44,38 @@ export class CSSResult {
3744
}
3845
}
3946

47+
/**
48+
* Wrap a value for interpolation in a css tagged template literal.
49+
*
50+
* This is unsafe because untrusted CSS text can be used to phone home
51+
* or exfiltrate data to an attacker controlled site. Take care to only use
52+
* this with trusted input.
53+
*/
54+
export const unsafeCss = (value: unknown) => {
55+
return new CSSResult(String(value), constructionToken);
56+
};
57+
4058
const textFromCSSResult = (value: CSSResult) => {
4159
if (value instanceof CSSResult) {
4260
return value.cssText;
4361
} else {
4462
throw new Error(
4563
`Value passed to 'css' function must be a 'css' function result: ${
46-
value}.`);
64+
value}. Use 'unsafeCss' to pass non-literal values, but
65+
take care to ensure page security.` );
4766
}
4867
};
4968

69+
/**
70+
* Template tag which which can be used with LitElement's `style` property to
71+
* set element styles. For security reasons, only literal string values may be
72+
* used. To incorporate non-literal values `unsafeCss` may be used inside a
73+
* template string part.
74+
*/
5075
export const css =
51-
(strings: TemplateStringsArray, ...values: CSSResult[]): CSSResult => {
76+
(strings: TemplateStringsArray, ...values: CSSResult[]) => {
5277
const cssText = values.reduce(
5378
(acc, v, idx) => acc + textFromCSSResult(v) + strings[idx + 1],
5479
strings[0]);
55-
return new CSSResult(cssText);
80+
return new CSSResult(cssText, constructionToken);
5681
};

src/test/lit-element_styling_test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import '@webcomponents/shadycss/apply-shim.min.js';
1616

1717
import {
1818
css,
19+
unsafeCss,
20+
CSSResult,
1921
html as htmlWithStyles,
2022
LitElement,
2123
} from '../lit-element.js';
@@ -432,6 +434,33 @@ suite('Static get styles', () => {
432434
assert.throws(() => { css`div { border: ${`2px solid blue;` as any}}`; });
433435
});
434436

437+
test('`CSSResult` cannot be constructed', async () => {
438+
// Note, this is done for security, instead use `css` or `unsafeCss`
439+
assert.throws(() => { new CSSResult('throw', Symbol()); });
440+
});
441+
442+
test('Any value can be used in `css` when included with `unsafeCss`', async () => {
443+
const name = generateElementName();
444+
const someVar = `2px solid blue`;
445+
customElements.define(name, class extends LitElement {
446+
static get styles() {
447+
return css`div {
448+
border: ${unsafeCss(someVar)};
449+
}`;
450+
}
451+
452+
render() {
453+
return htmlWithStyles`
454+
<div>Testing</div>`;
455+
}
456+
});
457+
const el = document.createElement(name);
458+
container.appendChild(el);
459+
await (el as LitElement).updateComplete;
460+
const div = el.shadowRoot!.querySelector('div');
461+
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '2px');
462+
});
463+
435464
test('styles in render compose with `static get styles`', async () => {
436465
const name = generateElementName();
437466
customElements.define(name, class extends LitElement {

0 commit comments

Comments
 (0)