Reflect.isTemplateObject (stage 2)
Authors: @mikesamuel, @koto Champions: @littledan, @ljharb Reviewers: @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
 
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 = sensitiveOperation`trusted0 ${ 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.
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 <bad argument> 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 = '<img onerror=alert(document.origin) src=x>';
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 
dodgyMarkdownToHTMLConverterto 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.
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.
You can browse the ecmarkup output or browse the source.
The test262 draft tests which would be added under test/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.