├── .npmrc ├── .gitattributes ├── CONTRIBUTING.md ├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── LICENSE ├── package.json ├── test └── sanity_check_test262_test.js ├── test262 └── test │ └── built-ins │ └── Reflect │ └── is-template-object.js ├── spec.emu └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | index.html -diff merge=ours 2 | spec.js -diff merge=ours 3 | spec.css -diff merge=ours 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Issues and PRs welcome. 2 | 3 | This is a TC39 proposal so is governed by the 4 | [TC39 Code of Conduct](https://github.com/tc39/code-of-conduct). 5 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build spec 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: ljharb/actions/node/install@main 12 | name: 'nvm install lts/* && npm install' 13 | with: 14 | node-version: lts/* 15 | - run: npm run build 16 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: ljharb/actions/node/install@main 15 | name: 'nvm install lts/* && npm install' 16 | with: 17 | node-version: lts/* 18 | - run: npm run build 19 | - uses: JamesIves/github-pages-deploy-action@v4.3.3 20 | with: 21 | branch: gh-pages 22 | folder: build 23 | clean: true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Only apps should have lockfiles 40 | yarn.lock 41 | package-lock.json 42 | npm-shrinkwrap.json 43 | 44 | # Emacs droppings 45 | *~ 46 | 47 | build 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mike Samuel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "proposal-array-is-template-object", 4 | "version": "0.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "npm run build-loose -- --watch", 9 | "build": "npm run build-loose -- --strict", 10 | "build-loose": "node -e 'fs.mkdirSync(\"build\", { recursive: true })' && ecmarkup --load-biblio @tc39/ecma262-biblio --verbose spec.emu build/index.html --lint-spec", 11 | "test": "mocha", 12 | "precommit": "npm run build && npm run test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/tc39/proposal-array-is-template-object.git" 17 | }, 18 | "author": "Mike Samuel", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/tc39/proposal-array-is-template-object/issues" 22 | }, 23 | "homepage": "https://github.com/tc39/proposal-array-is-template-object#readme", 24 | "devDependencies": { 25 | "@tc39/ecma262-biblio": "^2.1.2789", 26 | "chai": "^5.1.1", 27 | "ecmarkup": "^20.0.0", 28 | "is-template-object": "^1.0.1", 29 | "mocha": "^10.7.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/sanity_check_test262_test.js: -------------------------------------------------------------------------------- 1 | // Check that the test262 tests do something sensible when run against 2 | // a known problematic stub implementation of Reflect.isTemplateObject. 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const vm = require('vm'); 7 | const { expect } = require('chai'); 8 | 9 | describe('README.md', () => { 10 | describe('test262', () => { 11 | beforeEach(() => { 12 | Reflect.isTemplateObject = require('is-template-object').implementation; 13 | }); 14 | afterEach(() => { 15 | delete Reflect.isTemplateObject; 16 | }); 17 | 18 | it('runs, kind of', () => { 19 | const testContent = fs.readFileSync( 20 | path.join( 21 | __dirname, '..', 22 | 'test262', 'test', 'built-ins', 'Reflect', 'is-template-object.js'), 23 | { encoding: 'UTF-8' }); 24 | 25 | // Stub out some of the API provided at 26 | // https://github.com/tc39/test262/blob/master/INTERPRETING.md#host-defined-functions 27 | const test262stubErrorList = []; 28 | function $ERROR(msg) { 29 | test262stubErrorList[test262stubErrorList.length] = msg; 30 | } 31 | 32 | const $262 = { 33 | createRealm() { 34 | return { global: vm.runInNewContext('this') }; 35 | }, 36 | } 37 | 38 | // Evaluate the test content. 39 | new Function( 40 | '$ERROR', '$262', testContent)( 41 | ($ERROR), ($262)); 42 | 43 | expect(test262stubErrorList) 44 | .to.deep.equal([ 45 | '#2: Reflect.isTemplateObject producing spurious negative results:' 46 | + ' proxy,forgery,argument not poked' 47 | ]); 48 | }); 49 | }); 50 | }); 51 | 52 | 53 | -------------------------------------------------------------------------------- /test262/test/built-ins/Reflect/is-template-object.js: -------------------------------------------------------------------------------- 1 | /*--- 2 | info: | 3 | Reflect.isTemplateObject returns true given a 4 | template tag strings object and false otherwise. 5 | es5id: TBD 6 | description: Applies Reflect.isTemplateObject to various inputs. 7 | --*/ 8 | 9 | // A template tag that applies the function under test 10 | // and returns its result. 11 | function directTag(strings) { 12 | return Reflect.isTemplateObject(strings); 13 | } 14 | 15 | // A template tag that does the same but passes its 16 | // argument via normal function application. 17 | function indirectTag(strings) { 18 | return directTag(strings); 19 | } 20 | 21 | // A template object that escapes the tag function body. 22 | var escapedTemplateObject = null; 23 | ((x) => (escapedTemplateObject = x))`foo ${ null } bar`; 24 | 25 | var foreignTemplateObject = null; 26 | (() => { 27 | const realm = $262.createRealm(); 28 | foreignTemplateObject = 29 | (new realm.global.Function('return ((x) => x)`foreign strings`;'))(); 30 | })(); 31 | 32 | // Things that ought be recognized as template objects. 33 | // Elements are [ description, candidate value ] pairs. 34 | var posTestCases = [ 35 | [ 'direct', () => directTag`foo` ], 36 | // It doesn't matter whether the strings were used with the tag that's running. 37 | [ 'indirect', () => indirectTag`bar` ], 38 | // Or whether there is even a tag function on the stack. 39 | [ 'escaped', () => Reflect.isTemplateObject(escapedTemplateObject) ], 40 | [ 41 | 'called with null this', 42 | () => Reflect.apply(Reflect.isTemplateObject, null, [ escapedTemplateObject ]), 43 | ], 44 | // IsTemplateObject is realm-agnostic 45 | [ 46 | 'cross-realm template objects', 47 | () => Reflect.isTemplateObject(foreignTemplateObject), 48 | ], 49 | ]; 50 | 51 | var falsePositives = []; 52 | 53 | for (const [ message, f ] of posTestCases) { 54 | let result = null; 55 | try { 56 | result = f(); 57 | } catch (e) { 58 | falsePositives.push(message + ' threw'); 59 | continue; 60 | } 61 | if (result !== true) { 62 | falsePositives.push(message); 63 | } 64 | } 65 | 66 | // Things that should not be recognized as template objects. 67 | // Elements are [ description, candidate value ] pairs. 68 | var negTestCases = [ 69 | // Common values are not template string objects. 70 | [ 'zero args', () => directTag() ], 71 | [ 'null', () => directTag(null) ], 72 | [ 'undefined', () => directTag(undefined) ], 73 | [ 'zero', () => directTag(0) ], 74 | [ '-zero', () => directTag(-0) ], 75 | [ 'number', () => directTag(123) ], 76 | [ 'NaN', () => directTag(NaN) ], 77 | [ '+Inf', () => directTag(+Infinity) ], 78 | [ '-Inf', () => directTag(-Infinity) ], 79 | [ 'false', () => directTag(false) ], 80 | [ 'true', () => directTag(true) ], 81 | [ '{}', () => directTag({}) ], 82 | [ '[ "x" ]', () => directTag([ "x" ]) ], 83 | [ 'empty string', () => directTag('') ], 84 | [ 'string', () => directTag('foo') ], 85 | [ 'function', () => directTag(directTag) ], 86 | // A proxy over a template string object is not a template string object. 87 | [ 'proxy', () => directTag(new Proxy(escapedTemplateObject, {})) ], 88 | [ 'Array.prototype', () => Reflect.isTemplateObject(Array.prototype) ], 89 | // User code can't distinguish this case which is why this proposal adds value. 90 | [ 91 | 'forgery', 92 | () => { 93 | let arr = [ 'really really real' ]; 94 | Object.defineProperty(arr, 'raw', { value: arr }); 95 | Object.freeze(arr); 96 | return directTag(arr); 97 | } 98 | ], 99 | // The implementation shouldn't muck with its argument. 100 | [ 101 | 'argument not poked', () => { 102 | let poked = false; 103 | // Use a proxy to see if isTemplateObject 104 | // mucks with arg in an observable way. 105 | let arg = new Proxy( 106 | [], 107 | // The proxy handler is itself a proxy which 108 | // flips the poked bit if any proxy trap is 109 | // invoked. 110 | new Proxy( 111 | {}, 112 | { 113 | has(...args) { 114 | poked = true; 115 | return Reflect.has(...args); 116 | }, 117 | get(...args) { 118 | poked = true; 119 | return Reflect.get(...args); 120 | }, 121 | getPropertyDescriptor(...args) { 122 | poked = true; 123 | return Reflect.getPropertyDescriptor(...args); 124 | }, 125 | getPrototypeOf(...args) { 126 | poked = true; 127 | return Reflect.getPrototypeOf(...args); 128 | }, 129 | })); 130 | return Reflect.isTemplateObject(arg) || poked; 131 | } 132 | ], 133 | // Since a motivating use case is to identify strings that 134 | // originated within the current origin, the idiom from the spec note 135 | // shouldn't return true for a template object that originated in a 136 | // different realm. 137 | [ 138 | 'same-realm template object idiom', 139 | () => 140 | Reflect.isTemplateObject(foreignTemplateObject) 141 | && foreignTemplateObject instanceof Array, 142 | ], 143 | ]; 144 | 145 | var falseNegatives = []; 146 | 147 | for (const [ message, f ] of negTestCases) { 148 | let result = null; 149 | try { 150 | result = f(); 151 | } catch (e) { 152 | falseNegatives.push(message + ' threw'); 153 | continue; 154 | } 155 | if (result !== false) { 156 | falseNegatives.push(message); 157 | } 158 | } 159 | 160 | if (falsePositives.length) { 161 | $ERROR(`#1: Reflect.isTemplateObject producing spurious positive results: ${ falsePositives }`); 162 | } 163 | if (falseNegatives.length) { 164 | $ERROR(`#2: Reflect.isTemplateObject producing spurious negative results: ${ falseNegatives }`); 165 | } 166 | if (typeof Reflect.isTemplateObject !== 'function') { 167 | $ERROR('#3: Reflect.isTemplateObject has wrong typeof'); 168 | } 169 | if (Reflect.isTemplateObject.length !== 1) { 170 | $ERROR('#4: Reflect.isTemplateObject has wrong length'); 171 | } 172 | if (Reflect.isTemplateObject.name !== 'isTemplateObject') { 173 | $ERROR('#5: Reflect.isTemplateObject has wrong name'); 174 | } 175 | if (Object.prototype.toString.call(Reflect.isTemplateObject) !== '[object Function]') { 176 | $ERROR('#6: Reflect.isTemplateObject is not a normal function'); 177 | } 178 | -------------------------------------------------------------------------------- /spec.emu: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
  7 | title: Reflect.isTemplateObject
  8 | stage: 2
  9 | copyright: false
 10 | contributors: Mike Samuel, Krzysztof Kotowicz, Jordan Harband, Daniel Ehrenberg
 11 | 
12 | 13 | 14 |

Ordinary and Exotic Objects Behaviours

15 | 16 | 17 |

Built-in Exotic Object Internal Methods and Slots

18 | 19 | 20 |

Array Exotic Objects

21 | 22 | 23 |

24 | ArrayCreate ( 25 | _length_: a non-negative integer, 26 | optional _proto_: an Object, 27 | ): either a normal completion containing an Array exotic object or a throw completion 28 |

29 |
30 |
description
31 |
It is used to specify the creation of new Arrays.
32 |
skip global checks
33 |
true
34 |
35 | 36 | To enable the new API, this proposal modifies ArrayCreate to add an internal slot to each array. 37 | 38 | 39 | 1. If _length_ > 232 - 1, throw a *RangeError* exception. 40 | 1. If _proto_ is not present, set _proto_ to %Array.prototype%. 41 | 1. Let _A_ be MakeBasicObject(« [[Prototype]], [[Extensible]], [[TemplateObject]] »). 42 | 1. Set _A_.[[Prototype]] to _proto_. 43 | 1. Set _A_.[[TemplateObject]] to *false*. 44 | 1. Set _A_.[[DefineOwnProperty]] as specified in . 45 | 1. Perform ! OrdinaryDefineOwnProperty(_A_, *"length"*, PropertyDescriptor { [[Value]]: 𝔽(_length_), [[Writable]]: *true*, [[Enumerable]]: *false*, [[Configurable]]: *false* }). 46 | 1. Return _A_. 47 | 48 |
49 |
50 |
51 |
52 | 53 | 54 |

ECMAScript Language: Expressions

55 | 56 | 57 |

Primary Expression

58 | 59 | 60 |

Template Literals

61 | 62 | 63 |

64 | GetTemplateObject ( 65 | _templateLiteral_: a Parse Node, 66 | ): an Array 67 |

68 |
69 |
70 | 71 | To enable the new API, this proposal modifies GetTemplateObject to change the value of the new internal slot. 72 | 73 | 74 | 1. Let _realm_ be the current Realm Record. 75 | 1. Let _templateRegistry_ be _realm_.[[TemplateMap]]. 76 | 1. For each element _e_ of _templateRegistry_, do 77 | 1. If _e_.[[Site]] is the same Parse Node as _templateLiteral_, then 78 | 1. Return _e_.[[Array]]. 79 | 1. Let _rawStrings_ be the TemplateStrings of _templateLiteral_ with argument *true*. 80 | 1. Assert: _rawStrings_ is a List of Strings. 81 | 1. Let _cookedStrings_ be the TemplateStrings of _templateLiteral_ with argument *false*. 82 | 1. Let _count_ be the number of elements in the List _cookedStrings_. 83 | 1. Assert: _count_ ≤ 232 - 1. 84 | 1. Let _template_ be ! ArrayCreate(_count_). 85 | 1. Set _template_.[[TemplateObject]] to *true*. 86 | 1. Let _rawObj_ be ! ArrayCreate(_count_). 87 | 1. Let _index_ be 0. 88 | 1. Repeat, while _index_ < _count_, 89 | 1. Let _prop_ be ! ToString(𝔽(_index_)). 90 | 1. Let _cookedValue_ be _cookedStrings_[_index_]. 91 | 1. Perform ! DefinePropertyOrThrow(_template_, _prop_, PropertyDescriptor { [[Value]]: _cookedValue_, [[Writable]]: *false*, [[Enumerable]]: *true*, [[Configurable]]: *false* }). 92 | 1. Let _rawValue_ be the String value _rawStrings_[_index_]. 93 | 1. Perform ! DefinePropertyOrThrow(_rawObj_, _prop_, PropertyDescriptor { [[Value]]: _rawValue_, [[Writable]]: *false*, [[Enumerable]]: *true*, [[Configurable]]: *false* }). 94 | 1. Set _index_ to _index_ + 1. 95 | 1. Perform ! SetIntegrityLevel(_rawObj_, ~frozen~). 96 | 1. Perform ! DefinePropertyOrThrow(_template_, *"raw"*, PropertyDescriptor { [[Value]]: _rawObj_, [[Writable]]: *false*, [[Enumerable]]: *false*, [[Configurable]]: *false* }). 97 | 1. Perform ! SetIntegrityLevel(_template_, ~frozen~). 98 | 1. Append the Record { [[Site]]: _templateLiteral_, [[Array]]: _template_ } to _realm_.[[TemplateMap]]. 99 | 1. Return _template_. 100 | 101 | 102 |

The creation of a template object cannot result in an abrupt completion.

103 |
104 | 105 |

Each |TemplateLiteral| in the program code of a realm is associated with a unique template object that is used in the evaluation of tagged Templates (). The template objects are frozen and the same template object is used each time a specific tagged Template is evaluated. Whether template objects are created lazily upon first evaluation of the |TemplateLiteral| or eagerly prior to first evaluation is an implementation choice that is not observable to ECMAScript code.

106 |
107 | 108 |

Future editions of this specification may define additional non-enumerable properties of template objects.

109 |
110 |
111 |
112 |
113 |
114 | 115 | 116 |

Reflection

117 | 118 | 119 |

The Reflect Object

120 | 121 | 122 |

Reflect.isTemplateObject ( _value_ )

123 | 124 |

When the `isTemplateObject` method is called with argument _value_ the following steps are taken:

125 | 126 | 127 | 1. If ? IsArray(_value_) is *true* and _value_.[[TemplateObject]] is *true*, then 128 | 1. Return *true*. 129 | 1. Return *false*. 130 | 131 | 132 | 133 |

IsTemplateObject is realm-agnostic. Since template objects are frozen before escaping GetTemplateObject, testing (IsTemplateObject(_x_) and _x_.[[Prototype]] is the _realm_'s %Array.prototype%) is sufficient to determine whether an _x_ is a template object in a particular _realm_.

134 |

In user code, `Reflect.isTemplateObject(x) && x instanceof Array` is an equivalent test, assuming no changes to builtins.

135 |
136 |
137 |
138 |
139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reflect.isTemplateObject (stage [2](https://tc39.es/process-document/)) 2 | 3 | Authors: [@mikesamuel](https://github.com/mikesamuel), [@koto](https://github.com/koto) 4 | Champions: [@littledan](https://github.com/littledan), [@ljharb](https://github.com/ljharb) 5 | Reviewers: [@erights](https://github.com/erights), [@jridgewell](https://github.com/jridgewell) 6 | 7 | Provides a way for template tag functions to tell whether they were called with a template string bundle. 8 | 9 | **Table of Contents** 10 | 11 | * [Use cases & Prior Discussions](#use-cases--prior-discussions) 12 | + [Distinguishing strings from a trusted developer from strings that may be attacker controlled](#distinguishing-strings-from-a-trusted-developer-from-strings-that-may-be-attacker-controlled) 13 | * [An example](#an-example) 14 | * [What this is not](#what-this-is-not) 15 | * [Possible Spec Language](#possible-spec-language) 16 | * [Polyfill](#polyfill) 17 | * [Tests](#tests) 18 | * [Related Work](#related-work) 19 | 20 | 21 | ## Use cases & Prior Discussions 22 | 23 | ### Distinguishing strings from a trusted developer from strings that may be attacker controlled 24 | 25 | Issue [WICG/trusted-types#96](https://github.com/WICG/trusted-types/issues/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. 26 | 27 | ```js 28 | result = sensitiveOperation`trusted0 ${ untrusted } trusted1` 29 | // Authored by dev ^^^^^^^^ ^^^^^^^^ 30 | // May come from outside ^^^^^^^^^ 31 | ``` 32 | 33 | This proposal would provide enough context to warn or error out when this is not the case. 34 | 35 | ```js 36 | function (trustedStrings, ...untrustedArguments) { 37 | if (Reflect.isTemplateObject(trustedStrings) 38 | // instanceof provides a same-Realm guarantee for early frozen objects. 39 | && trustedStrings instanceof Array) { 40 | // Proceed knowing that trustedStrings come from 41 | // the JavaScript module's authors. 42 | } else { 43 | // Do not trust trustedStrings 44 | } 45 | } 46 | ``` 47 | 48 | This assumes that an attacker cannot get a string to `eval` or `new Function` as in 49 | 50 | ```js 51 | const attackerControlledString = '((x) => x)`evil string`'; 52 | 53 | // Naive code 54 | let x = eval(attackerControlledString) 55 | 56 | console.log(Reflect.isTemplateObject(x)); 57 | ``` 58 | 59 | Many other security assumptions break if an attacker can execute arbitrary code, so this check is still useful. 60 | 61 | ## An Example 62 | 63 | Here's an example of how `isTemplateObject` lets a tag function wisely use a sensitive operation, namely *[Create a Trusted Type][]*. 64 | The sensitive operation is not directly accessible to the tag function's callers since it's in a local scope. 65 | This assumes that TT's [first-come-first-serve name restrictions][TT-block] solve provisioning, letting only authorized callers access the sensitive operation. 66 | 67 | ```js 68 | const { Array, Reflect, TypeError } = globalThis; 69 | const { createPolicy } = trustedTypes; 70 | const { isTemplateObject } = Reflect; 71 | const { error: consoleErr } = console; 72 | 73 | /** 74 | * A tag function that produces *TrustedHTML* or null if the 75 | * policy name "trustedHTMLTagFunction" is not available. 76 | */ 77 | export trustedHTML = (() => { 78 | // We use TrustedType's first-come-first-serve policy name restrictions 79 | // to provision this scope with sensitiveOperation. 80 | const policyName = 'trustedHTMLTagFunction'; 81 | let policy; 82 | try { 83 | policy = createPolicy( 84 | 'trustedHTMLTagFunction', 85 | { createHTML(s) { return s } } 86 | ); 87 | } catch (ex) { 88 | consoleErr(`${policyName} is not an allowed trustedTypes policy name`); 89 | return null; 90 | } 91 | 92 | // This is the sensitive operation. 93 | const { createHTML } = policy; 94 | 95 | // This tag function uses isTemplateObject to reject strings that 96 | // do not appear in user code in the same realm. 97 | // 98 | // With a reliable isTemplateObject check, the attack surface is 99 | // <= |set of template applications in trusted code|. 100 | // 101 | // That set is finite. 102 | // 103 | // Without a reliable isTemplateObject check, the attack surface is 104 | // <= |set of attacker controlled strings|. That is, in practice, 105 | // unbounded. 106 | // 107 | // This assumes no attacker has eval. 108 | const trustedHTMLTagFunction = (strings) => { 109 | if (isTemplateObject(strings) && strings instanceof Array) { 110 | return createHTML(strings.raw[0]); 111 | } 112 | throw new TypeError("Expected template object"); 113 | }; 114 | 115 | // With the check it's safe to export this tag function that closes 116 | // over a sensitive operation to anyone. 117 | return trustedHTMLTagFunction; 118 | })() 119 | ``` 120 | 121 | Without `isArrayTemplate`, this can be bypassed: 122 | 123 | ```js 124 | // A naive, but non-malicious function. 125 | function f(x) { 126 | // People trust trustedHTMLTagFunction. 127 | // Our HTML is trustworthy because so we'll just 128 | // piggyback off that by using a value that looks like a template object. 129 | // What could possibly go wrong? 130 | const s = dodgyMarkdownToHTMLConverter(x); 131 | const pseudoTemplateObject = [s]; 132 | pseudoTemplateObject.raw = Object.freeze([s]); 133 | return trustedHTML(Object.freeze(pseudoTemplateObject)); 134 | } 135 | 136 | // An attacker controlled string reaches f(). 137 | const payload = ''; 138 | console.log(`f(${ JSON.stringify(payload) }) = ${ f(payload) }`); 139 | ``` 140 | 141 | The threat model here involves three actors: 142 | * A team of *first-party developers* (in conjunction with security specialists) decides to trust the tag function. 143 | * A malicious *attacker* controls a string in the variable `payload`. 144 | * Non-malicious but confusable third-party library tries to provide a higher level of service by forging a template object. 145 | It assumes its clients are comfortable with trusting `dodgyMarkdownToHTMLConverter` to produce HTML for the current origin. 146 | 147 | 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. 148 | 149 | This simple implementation doesn't deal with interpolations. 150 | A more thorough implementation could do [contextual autoescaping][]. 151 | 152 | ## What this is not 153 | 154 | This is not an attempt to determine whether the current function was called as a template literal. 155 | 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. 156 | 157 | ## Possible Spec Language 158 | 159 | You can browse the [ecmarkup output](https://tc39.es/proposal-array-is-template-object/) or browse the [source](https://github.com/tc39/proposal-array-is-template-object/blob/master/spec.emu). 160 | 161 | 168 | 169 | ## Tests 170 | 171 | The test262 172 | [draft tests](https://github.com/tc39/proposal-array-is-template-object/blob/master/test262/test/built-ins/Reflect/is-template-object.js) 173 | which would be added under 174 | [test/built-ins/Reflect](https://github.com/tc39/test262/tree/master/test/built-ins/Reflect) 175 | 176 | ## Related Work 177 | 178 | If the [literals proposal](https://github.com/mikewest/tc39-proposal-literals) were to advance, this proposal would be unnecessary since they both cover the use cases from this document. 179 | 180 | [contextual autoescaping]: https://rawgit.com/mikesamuel/sanitized-jquery-templates/trunk/safetemplate.html 181 | [TT-block]: https://w3c.github.io/webappsec-trusted-types/dist/spec/#abstract-opdef-should-trusted-type-policy-creation-be-blocked-by-content-security-policy 182 | [Create a Trusted Type]: https://w3c.github.io/webappsec-trusted-types/dist/spec/#create-a-trusted-type-algorithm 183 | --------------------------------------------------------------------------------