Skip to content

Commit

Permalink
Add an additional security brand check to StaticValues (#2642)
Browse files Browse the repository at this point in the history
Similar to #2307
  • Loading branch information
rictic committed Mar 23, 2022
1 parent a6069c4 commit badc532
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 7 deletions.
6 changes: 6 additions & 0 deletions .changeset/seven-bottles-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'lit-html': patch
'lit': patch
---

Add an additional security brand check to StaticValues; Similar to [#2307](https://github.com/lit/lit/pull/2307)
50 changes: 43 additions & 7 deletions packages/lit-html/src/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,43 @@

import {html as coreHtml, svg as coreSvg, TemplateResult} from './lit-html.js';

interface StaticValue {
/** The value to interpolate as-is into the template. */
_$litStatic$: string;

/**
* A value that can't be decoded from ordinary JSON, make it harder for
* a attacker-controlled data that goes through JSON.parse to produce a valid
* StaticValue.
*/
r: typeof brand;
}

/**
* Prevents JSON injection attacks.
*
* The goals of this brand:
* 1) fast to check
* 2) code is small on the wire
* 3) multiple versions of Lit in a single page will all produce mutually
* interoperable StaticValues
* 4) normal JSON.parse (without an unusual reviver) can not produce a
* StaticValue
*
* Symbols satisfy (1), (2), and (4). We use Symbol.for to satisfy (3), but
* we don't care about the key, so we break ties via (2) and use the empty
* string.
*/
const brand = Symbol.for('');

/** Safely extracts the string part of a StaticValue. */
const unwrapStaticValue = (value: unknown): string | undefined => {
if ((value as Partial<StaticValue>)?.r !== brand) {
return undefined;
}
return (value as Partial<StaticValue>)?.['_$litStatic$'];
};

/**
* Wraps a string so that it behaves like part of the static template
* strings instead of a dynamic value.
Expand All @@ -23,8 +60,9 @@ import {html as coreHtml, svg as coreSvg, TemplateResult} from './lit-html.js';
* Static values can be changed, but they will cause a complete re-render
* since they effectively create a new template.
*/
export const unsafeStatic = (value: string) => ({
export const unsafeStatic = (value: string): StaticValue => ({
['_$litStatic$']: value,
r: brand,
});

const textFromStatic = (value: StaticValue) => {
Expand Down Expand Up @@ -55,15 +93,14 @@ const textFromStatic = (value: StaticValue) => {
export const literal = (
strings: TemplateStringsArray,
...values: unknown[]
) => ({
): StaticValue => ({
['_$litStatic$']: values.reduce(
(acc, v, idx) => acc + textFromStatic(v as StaticValue) + strings[idx + 1],
strings[0]
),
) as string,
r: brand,
});

type StaticValue = ReturnType<typeof unsafeStatic>;

const stringsCache = new Map<string, TemplateStringsArray>();

/**
Expand All @@ -89,8 +126,7 @@ export const withStatic =
while (
i < l &&
((dynamicValue = values[i]),
(staticValue = (dynamicValue as StaticValue)?.['_$litStatic$'])) !==
undefined
(staticValue = unwrapStaticValue(dynamicValue))) !== undefined
) {
s += staticValue + strings[++i];
hasStatics = true;
Expand Down
13 changes: 13 additions & 0 deletions packages/lit-html/src/test/static_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,17 @@ suite('static', () => {
);
});
});

test(`don't render simple spoofed static values`, () => {
const spoof = {
['_$staticValue$']: 'foo',
r: {},
};
const template = html`<div>${spoof}</div>`;
render(template, container);
assert.equal(
stripExpressionComments(container.innerHTML),
'<div>[object Object]</div>'
);
});
});

0 comments on commit badc532

Please sign in to comment.