GitHub - tc39/proposal-array-is-template-object: TC39 proposal to identify tagged template string array objects (original) (raw)
Reflect.isTemplateObject (stage 2)
Authors: @mikesamuel, @kotoChampions: @littledan, @ljharbReviewers: @erights, @jridgewell
Provides a way for template tag functions to tell whether they were called with a template string bundle.
Table of Contents
- Use cases & Prior Discussions
- An example
- What this is not
- Possible Spec Language
- Polyfill
- Tests
- Related Work
Use cases & Prior Discussions
Distinguishing strings from a trusted developer from strings that may be attacker controlled
Issue WICG/trusted-types#96 describes a scenario where a template tag assumes that the literal strings were authored by a trusted developer but that the interpolated values may not be.
result = sensitiveOperationtrusted0 ${ untrusted } trusted1
// Authored by dev ^^^^^^^^ ^^^^^^^^
// May come from outside ^^^^^^^^^
This proposal would provide enough context to warn or error out when this is not the case.
function (trustedStrings, ...untrustedArguments) { if (Reflect.isTemplateObject(trustedStrings) // instanceof provides a same-Realm guarantee for early frozen objects. && trustedStrings instanceof Array) { // Proceed knowing that trustedStrings come from // the JavaScript module's authors. } else { // Do not trust trustedStrings } }
This assumes that an attacker cannot get a string to eval
or new Function
as in
const attackerControlledString = '((x) => x)evil string
';
// Naive code let x = eval(attackerControlledString)
console.log(Reflect.isTemplateObject(x));
Many other security assumptions break if an attacker can execute arbitrary code, so this check is still useful.
An Example
Here's an example of how isTemplateObject
lets a tag function wisely use a sensitive operation, namely Create a Trusted Type. The sensitive operation is not directly accessible to the tag function's callers since it's in a local scope. This assumes that TT's first-come-first-serve name restrictions solve provisioning, letting only authorized callers access the sensitive operation.
const { Array, Reflect, TypeError } = globalThis; const { createPolicy } = trustedTypes; const { isTemplateObject } = Reflect; const { error: consoleErr } = console;
/**
- A tag function that produces TrustedHTML or null if the
- policy name "trustedHTMLTagFunction" is not available.
*/
export trustedHTML = (() => {
// We use TrustedType's first-come-first-serve policy name restrictions
// to provision this scope with sensitiveOperation.
const policyName = 'trustedHTMLTagFunction';
let policy;
try {
policy = createPolicy(
'trustedHTMLTagFunction',
{ createHTML(s) { return s } }
);
} catch (ex) {
consoleErr(
${policyName} is not an allowed trustedTypes policy name
); return null; }
// This is the sensitive operation. const { createHTML } = policy;
// This tag function uses isTemplateObject to reject strings that // do not appear in user code in the same realm. // // With a reliable isTemplateObject check, the attack surface is // <= |set of template applications in trusted code|. // // That set is finite. // // Without a reliable isTemplateObject check, the attack surface is // <= |set of attacker controlled strings|. That is, in practice, // unbounded. // // This assumes no attacker has eval. const trustedHTMLTagFunction = (strings) => { if (isTemplateObject(strings) && strings instanceof Array) { return createHTML(strings.raw[0]); } throw new TypeError("Expected template object"); };
// With the check it's safe to export this tag function that closes // over a sensitive operation to anyone. return trustedHTMLTagFunction; })()
Without isArrayTemplate
, this can be bypassed:
// A naive, but non-malicious function. function f(x) { // People trust trustedHTMLTagFunction. // Our HTML is trustworthy because so we'll just // piggyback off that by using a value that looks like a template object. // What could possibly go wrong? const s = dodgyMarkdownToHTMLConverter(x); const pseudoTemplateObject = [s]; pseudoTemplateObject.raw = Object.freeze([s]); return trustedHTML(Object.freeze(pseudoTemplateObject)); }
// An attacker controlled string reaches f().
const payload = '';
console.log(
f(${ JSON.stringify(payload) }) = ${ f(payload) }
);
The threat model here involves three actors:
- A team of first-party developers (in conjunction with security specialists) decides to trust the tag function.
- A malicious attacker controls a string in the variable
payload
. - Non-malicious but confusable third-party library tries to provide a higher level of service by forging a template object. It assumes its clients are comfortable with trusting
dodgyMarkdownToHTMLConverter
to produce HTML for the current origin.
We've addressed this threat model when the first-party developers can be less tolerant of risk than the most risk tolerant third party dependency w.r.t. HTML injection.
This simple implementation doesn't deal with interpolations. A more thorough implementation could do contextual autoescaping.
What this is not
This is not an attempt to determine whether the current function was called as a template literal. See the linked issue as to why that is untenable. Especially the discussion around threat models, eval
, and tail-call optimizations that weighed against alternate approaches.
Possible Spec Language
You can browse the ecmarkup output or browse the source.
Tests
The test262draft testswhich would be added undertest/built-ins/Reflect
Related Work
If the literals proposal were to advance, this proposal would be unnecessary since they both cover the use cases from this document.