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

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;

/**

// 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:

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

If the literals proposal were to advance, this proposal would be unnecessary since they both cover the use cases from this document.