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.&&trustedStringsinstanceofArray){// 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
constattackerControlledString='((x) => x)`evil string`';// Naive codeletx=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. */exporttrustedHTML=(()=>{// We use TrustedType's first-come-first-serve policy name restrictions// to provision this scope with sensitiveOperation.constpolicyName='trustedHTMLTagFunction';letpolicy;try{policy=createPolicy('trustedHTMLTagFunction',{createHTML(s){returns}});}catch(ex){consoleErr(`${policyName} is not an allowed trustedTypes policy name`);returnnull;}// 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.consttrustedHTMLTagFunction=(strings)=>{if(isTemplateObject(strings)&&stringsinstanceofArray){returncreateHTML(strings.raw[0]);}thrownewTypeError("Expected template object");};// With the check it's safe to export this tag function that closes// over a sensitive operation to anyone.returntrustedHTMLTagFunction;})()Without isArrayTemplate, this can be bypassed:
// A naive, but non-malicious function.functionf(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?consts=dodgyMarkdownToHTMLConverter(x);constpseudoTemplateObject=[s];pseudoTemplateObject.raw=Object.freeze([s]);returntrustedHTML(Object.freeze(pseudoTemplateObject));}// An attacker controlled string reaches f().constpayload='<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.