From 85f5113d961e6c62161cd78302ced3165a4c1464 Mon Sep 17 00:00:00 2001 From: Nils Knappmeier Date: Mon, 4 Nov 2019 23:04:51 +0100 Subject: [PATCH 1/4] feat: additional security measures Use map-objects (with `null`-prototype for some internal maps that are accessed by the compiled template code to prevent accidental access of Object prototype properties (this includes the "helpers"-object, "partials"-object and "knownHelpers") Added compile options: - allowNonHelperFunctionCall: default: true, if set to false, the template will not call functions with arguments) that are defined on the current context object. Lambdas (function calls without arguments) are still allowed. - propertyMustBeEnumerable a object (propertyName: boolean) of properties that must be enumerable in order to be resolved from the current context object. By default `__defineGetter__`, `__defineSetter__` , `__proto__` and `constructor` are forbidden (if not enumerable) and will return "undefined" instead of the actual property. Use `{ __defineGetter_: false }` to allow __defineGetter__ (not recomended). --- lib/handlebars/compiler/compiler.js | 15 ++-- .../compiler/javascript-compiler.js | 23 +++++-- lib/handlebars/internal/utils.js | 20 ++++++ lib/handlebars/runtime.js | 7 +- spec/security.js | 69 ++++++++++++++++--- types/index.d.ts | 6 ++ 6 files changed, 117 insertions(+), 23 deletions(-) create mode 100644 lib/handlebars/internal/utils.js diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 894f7a7c0..381ab8f5a 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -3,6 +3,7 @@ import Exception from '../exception'; import {isArray, indexOf, extend} from '../utils'; import AST from './ast'; +import {newMapObject} from '../internal/utils'; const slice = [].slice; @@ -56,7 +57,8 @@ Compiler.prototype = { // These changes will propagate to the other compiler components let knownHelpers = options.knownHelpers; - options.knownHelpers = { + + options.knownHelpers = newMapObject({ 'helperMissing': true, 'blockHelperMissing': true, 'each': true, @@ -65,14 +67,11 @@ Compiler.prototype = { 'with': true, 'log': true, 'lookup': true - }; + }); if (knownHelpers) { - // the next line should use "Object.keys", but the code has been like this a long time and changing it, might - // cause backwards-compatibility issues... It's an old library... - // eslint-disable-next-line guard-for-in - for (let name in knownHelpers) { - this.options.knownHelpers[name] = knownHelpers[name]; - } + Object.keys(knownHelpers).forEach(name => { + this.options.knownHelpers[name] = knownHelpers[name]; + }); } return this.accept(program); diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index b21df1d8a..8067aaad4 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -1,6 +1,6 @@ import { COMPILER_REVISION, REVISION_CHANGES } from '../base'; import Exception from '../exception'; -import {isArray} from '../utils'; +import {extend, isArray} from '../utils'; import CodeGen from './code-gen'; function Literal(value) { @@ -13,9 +13,9 @@ JavaScriptCompiler.prototype = { // PUBLIC API: You can override these methods in a subclass to provide // alternative compiled forms for name lookup and buffering semantics nameLookup: function(parent, name/* , type*/) { - const isEnumerable = [ this.aliasable('container.propertyIsEnumerable'), '.call(', parent, ',"constructor")']; + const isEnumerable = [ this.aliasable('container.propertyIsEnumerable'), '.call(', parent, ',', JSON.stringify(name), ')']; - if (name === 'constructor') { + if (this.propertyMustBeEnumerable[name]) { return ['(', isEnumerable, '?', _actualLookup(), ' : undefined)']; } return _actualLookup(); @@ -93,6 +93,13 @@ JavaScriptCompiler.prototype = { this.useDepths = this.useDepths || environment.useDepths || environment.useDecorators || this.options.compat; this.useBlockParams = this.useBlockParams || environment.useBlockParams; + this.allowNonHelperFunctionCall = this.options.allowNonHelperFunctionCall !== false; // default if true for 4.x + this.propertyMustBeEnumerable = extend({ + __defineGetter__: true, + __defineSetter__: true, + __proto__: true, + constructor: true + }, this.options.propertyMustBeEnumerable); let opcodes = environment.opcodes, opcode, @@ -634,12 +641,20 @@ JavaScriptCompiler.prototype = { if (isSimple) { // direct call to helper possibleFunctionCalls.push(helper.name); } + // call a function from the input object - possibleFunctionCalls.push(nonHelper); + if (this.allowNonHelperFunctionCall) { + possibleFunctionCalls.push(nonHelper); + } + if (!this.options.strict) { possibleFunctionCalls.push(this.aliasable('container.hooks.helperMissing')); } + if (possibleFunctionCalls.length === 0) { + throw new Exception('Cannot create code for calling "' + name + '": Non-helper function calls are not allowed.'); + } + let functionLookupCode = ['(', this.itemsSeparatedBy(possibleFunctionCalls, '||'), ')']; let functionCall = this.source.functionCall(functionLookupCode, 'call', helper.callParams); this.push(functionCall); diff --git a/lib/handlebars/internal/utils.js b/lib/handlebars/internal/utils.js new file mode 100644 index 000000000..00251fe47 --- /dev/null +++ b/lib/handlebars/internal/utils.js @@ -0,0 +1,20 @@ +/** + * Create a new object with `null`-constructor containing all enumerable properties of all source objects. + * + * Intention: omit potentially malicious prototype properties + * + * @private + * @param {...object} sourceObjects + * @returns {object} + */ +export function newMapObject(...sourceObjects) { + let result = Object.create(null); + sourceObjects.forEach(sourceObject => { + if (sourceObject != null) { + Object.keys(sourceObject).forEach(key => { + result[key] = sourceObject[key]; + }); + } + }); + return result; +} diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 958afc3ef..d2e710806 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -2,6 +2,7 @@ import * as Utils from './utils'; import Exception from './exception'; import {COMPILER_REVISION, createFrame, LAST_COMPATIBLE_COMPILER_REVISION, REVISION_CHANGES} from './base'; import {moveHelperToHooks} from './helpers'; +import {newMapObject} from './internal/utils'; export function checkRevision(compilerInfo) { const compilerRevision = compilerInfo && compilerInfo[0] || 1, @@ -158,13 +159,13 @@ export function template(templateSpec, env) { ret._setup = function(options) { if (!options.partial) { - container.helpers = Utils.extend({}, env.helpers, options.helpers); + container.helpers = newMapObject(env.helpers, options.helpers); if (templateSpec.usePartial) { - container.partials = Utils.extend({}, env.partials, options.partials); + container.partials = newMapObject(env.partials, options.partials); } if (templateSpec.usePartial || templateSpec.useDecorators) { - container.decorators = Utils.extend({}, env.decorators, options.decorators); + container.decorators = newMapObject(env.decorators, options.decorators); } container.hooks = {}; diff --git a/spec/security.js b/spec/security.js index 876b67292..88e572f94 100644 --- a/spec/security.js +++ b/spec/security.js @@ -113,14 +113,67 @@ describe('security issues', function() { describe('GH-1563', function() { it('should not allow to access constructor after overriding via __defineGetter__', function() { - if (({}).__defineGetter__ == null || ({}).__lookupGetter__ == null) { - return; // Browser does not support this exploit anyway - } - shouldCompileTo('{{__defineGetter__ "undefined" valueOf }}' + - '{{#with __lookupGetter__ }}' + - '{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}' + - '{{constructor.name}}' + - '{{/with}}', {}, ''); + + shouldThrow(function() { + compileWithPartials('{{__defineGetter__ "undefined" valueOf }}' + + '{{#with __lookupGetter__ }}' + + '{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}' + + '{{constructor.name}}' + + '{{/with}}', [{}, {}, {}, {}])({}); + } + ); + }); + }); + + describe('the compile option "allowNonHelperFunctionCall"', function() { + it('when set to false should prevent calling functions in input objects', function() { + shouldThrow(function() { + var template = compileWithPartials('{{test abc}}', [{}, {}, {}, {allowNonHelperFunctionCall: false}]); + template({test: function() { return 'abc'; }}); + }, null, /Missing helper/); + }); + it('when set to false should prevent calling functions in input objects (in strict mode)', function() { + shouldThrow(function() { + var template = compileWithPartials('{{obj.method abc}}', [{}, {}, {}, {allowNonHelperFunctionCall: false, strict: true}]); + template({}); + }, null, /Cannot create code.*obj\.method.*Non-helper/); + }); + + }); + + describe('Properties that are required to be enumerable', function() { + + it('access should be restricted if not enumerable', function() { + shouldCompileTo('{{__defineGetter__}}', {}, ''); + shouldCompileTo('{{__defineSetter__}}', {}, ''); + shouldCompileTo('{{__proto__}}', {}, ''); + shouldCompileTo('{{constructor}}', {}, ''); + }); + + it('access should be allowed if enumerable', function() { + shouldCompileTo('{{__defineGetter__}}', {__defineGetter__: 'abc'}, 'abc'); + shouldCompileTo('{{__defineSetter__}}', {__defineSetter__: 'abc'}, 'abc'); + shouldCompileTo('{{constructor}}', {constructor: 'abc'}, 'abc'); + }); + + it('access can be allowed via the compile-option "propertyMustBeEnumerable"', function() { + var context = {}; + ['__defineGetter__', '__defineSetter__'].forEach(function(property) { + Object.defineProperty(context, property, { + get: function() { + return property; + }, + enumerable: false + }); + }); + + var compileOptions = { + propertyMustBeEnumerable: { + __defineGetter__: false + } + }; + + shouldCompileTo('{{__defineGetter__}}{{__defineSetter__}}', [context, {}, {}, compileOptions], '__defineGetter__'); }); }); }); diff --git a/types/index.d.ts b/types/index.d.ts index bb0656a51..8a24eb1dc 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -214,6 +214,12 @@ interface CompileOptions { preventIndent?: boolean; ignoreStandalone?: boolean; explicitPartialContext?: boolean; + allowNonHelperFunctionCall?: boolean; + propertyMustBeEnumerable?: PropertyMustBeEnumerable; +} + +type PropertyMustBeEnumerable = { + [name: string]: boolean; } type KnownHelpers = { From 85cdfa605814bd7c7c69ad673590b023d043c926 Mon Sep 17 00:00:00 2001 From: Nils Knappmeier Date: Sun, 10 Nov 2019 21:58:13 +0100 Subject: [PATCH 2/4] make "propertyMustBeEnumberable" an map-object as well --- lib/handlebars/compiler/javascript-compiler.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 8067aaad4..824d6cbdf 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -1,7 +1,8 @@ import { COMPILER_REVISION, REVISION_CHANGES } from '../base'; import Exception from '../exception'; -import {extend, isArray} from '../utils'; +import {isArray} from '../utils'; import CodeGen from './code-gen'; +import {newMapObject} from '../internal/utils'; function Literal(value) { this.value = value; @@ -94,7 +95,7 @@ JavaScriptCompiler.prototype = { this.useDepths = this.useDepths || environment.useDepths || environment.useDecorators || this.options.compat; this.useBlockParams = this.useBlockParams || environment.useBlockParams; this.allowNonHelperFunctionCall = this.options.allowNonHelperFunctionCall !== false; // default if true for 4.x - this.propertyMustBeEnumerable = extend({ + this.propertyMustBeEnumerable = newMapObject({ __defineGetter__: true, __defineSetter__: true, __proto__: true, From 43d90c13a93a8da51eed08e985cfe60f37dc5a17 Mon Sep 17 00:00:00 2001 From: Nils Knappmeier Date: Sun, 10 Nov 2019 22:34:41 +0100 Subject: [PATCH 3/4] __proto__ can only be set on a map-object (with null-constructor) --- lib/handlebars/compiler/javascript-compiler.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 824d6cbdf..dd4b89bcb 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -95,12 +95,14 @@ JavaScriptCompiler.prototype = { this.useDepths = this.useDepths || environment.useDepths || environment.useDecorators || this.options.compat; this.useBlockParams = this.useBlockParams || environment.useBlockParams; this.allowNonHelperFunctionCall = this.options.allowNonHelperFunctionCall !== false; // default if true for 4.x - this.propertyMustBeEnumerable = newMapObject({ - __defineGetter__: true, - __defineSetter__: true, - __proto__: true, - constructor: true - }, this.options.propertyMustBeEnumerable); + + // Workaround: We cannot use {...}-notation to create this object, because that way, we cannot overwrite __proto__ + let propertyMustBeEnumerableDefaultValues = Object.create(null); + ['__defineGetter__', '__defineSetter__', '__proto__', 'constructor'].forEach((forbiddenProperty) => { + propertyMustBeEnumerableDefaultValues[forbiddenProperty] = true; + }); + + this.propertyMustBeEnumerable = newMapObject(propertyMustBeEnumerableDefaultValues, this.options.propertyMustBeEnumerable); let opcodes = environment.opcodes, opcode, From caacce4f54254d29220ce4a33f96a6fdf676a385 Mon Sep 17 00:00:00 2001 From: Nils Knappmeier Date: Sun, 17 Nov 2019 08:59:07 +0100 Subject: [PATCH 4/4] fixup: "lookup" should also respect dangerous properties - properties not configurable anymore - add more harmful properties to the list (push, pop, splice...) --- lib/handlebars/compiler/compiler.js | 4 +- .../compiler/javascript-compiler.js | 13 +--- lib/handlebars/helpers/lookup.js | 4 +- .../newObjectWithoutPrototypeProperties.js | 23 ++++++++ .../internal/propertyMustBeEnumerable.js | 59 +++++++++++++++++++ lib/handlebars/internal/utils.js | 20 ------- lib/handlebars/runtime.js | 8 +-- types/index.d.ts | 5 -- 8 files changed, 94 insertions(+), 42 deletions(-) create mode 100644 lib/handlebars/internal/newObjectWithoutPrototypeProperties.js create mode 100644 lib/handlebars/internal/propertyMustBeEnumerable.js delete mode 100644 lib/handlebars/internal/utils.js diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 381ab8f5a..9c08e9b0d 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -3,7 +3,7 @@ import Exception from '../exception'; import {isArray, indexOf, extend} from '../utils'; import AST from './ast'; -import {newMapObject} from '../internal/utils'; +import {newObjectWithoutPrototypeProperties} from '../internal/newObjectWithoutPrototypeProperties'; const slice = [].slice; @@ -58,7 +58,7 @@ Compiler.prototype = { // These changes will propagate to the other compiler components let knownHelpers = options.knownHelpers; - options.knownHelpers = newMapObject({ + options.knownHelpers = newObjectWithoutPrototypeProperties({ 'helperMissing': true, 'blockHelperMissing': true, 'each': true, diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index dd4b89bcb..9a60fbe30 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -2,7 +2,7 @@ import { COMPILER_REVISION, REVISION_CHANGES } from '../base'; import Exception from '../exception'; import {isArray} from '../utils'; import CodeGen from './code-gen'; -import {newMapObject} from '../internal/utils'; +import {propertyMustBeEnumerable} from '../internal/propertyMustBeEnumerable'; function Literal(value) { this.value = value; @@ -16,7 +16,7 @@ JavaScriptCompiler.prototype = { nameLookup: function(parent, name/* , type*/) { const isEnumerable = [ this.aliasable('container.propertyIsEnumerable'), '.call(', parent, ',', JSON.stringify(name), ')']; - if (this.propertyMustBeEnumerable[name]) { + if (propertyMustBeEnumerable(name)) { return ['(', isEnumerable, '?', _actualLookup(), ' : undefined)']; } return _actualLookup(); @@ -96,14 +96,6 @@ JavaScriptCompiler.prototype = { this.useBlockParams = this.useBlockParams || environment.useBlockParams; this.allowNonHelperFunctionCall = this.options.allowNonHelperFunctionCall !== false; // default if true for 4.x - // Workaround: We cannot use {...}-notation to create this object, because that way, we cannot overwrite __proto__ - let propertyMustBeEnumerableDefaultValues = Object.create(null); - ['__defineGetter__', '__defineSetter__', '__proto__', 'constructor'].forEach((forbiddenProperty) => { - propertyMustBeEnumerableDefaultValues[forbiddenProperty] = true; - }); - - this.propertyMustBeEnumerable = newMapObject(propertyMustBeEnumerableDefaultValues, this.options.propertyMustBeEnumerable); - let opcodes = environment.opcodes, opcode, firstLoc, @@ -1027,6 +1019,7 @@ JavaScriptCompiler.prototype = { let params = [], paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper); let foundHelper = this.nameLookup('helpers', name, 'helper'), + // NEXT-MAJOR-UPGRADE: Always use "nullContext" in Handlebars 5.0 callContext = this.aliasable(`${this.contextName(0)} != null ? ${this.contextName(0)} : (container.nullContext || {})`); return { diff --git a/lib/handlebars/helpers/lookup.js b/lib/handlebars/helpers/lookup.js index 0654cc393..c261b0517 100644 --- a/lib/handlebars/helpers/lookup.js +++ b/lib/handlebars/helpers/lookup.js @@ -1,9 +1,11 @@ +import {propertyMustBeEnumerable} from '../internal/propertyMustBeEnumerable'; + export default function(instance) { instance.registerHelper('lookup', function(obj, field) { if (!obj) { return obj; } - if (String(field) === 'constructor' && !obj.propertyIsEnumerable(field)) { + if (propertyMustBeEnumerable(field) && !obj.propertyIsEnumerable(field)) { return undefined; } return obj[field]; diff --git a/lib/handlebars/internal/newObjectWithoutPrototypeProperties.js b/lib/handlebars/internal/newObjectWithoutPrototypeProperties.js new file mode 100644 index 000000000..2976afc89 --- /dev/null +++ b/lib/handlebars/internal/newObjectWithoutPrototypeProperties.js @@ -0,0 +1,23 @@ +/** + * Create an object without Object.prototype methods like __defineGetter__, constructor, + * __defineSetter__ and __proto__. + * + * Those methods should not be accessed from template code, because that can lead to + * security leaks. This method should be used to create internal objects that. + * + * @private + * @param {...object} sourceObjects + * @returns {object} + */ +export function newObjectWithoutPrototypeProperties(...sourceObjects) { + let result = Object.create(null); + sourceObjects.forEach(sourceObject => { + if (sourceObject != null) { + Object.keys(sourceObject).forEach(key => { + result[key] = sourceObject[key]; + }); + } + }); + return result; +} + diff --git a/lib/handlebars/internal/propertyMustBeEnumerable.js b/lib/handlebars/internal/propertyMustBeEnumerable.js new file mode 100644 index 000000000..ad541acec --- /dev/null +++ b/lib/handlebars/internal/propertyMustBeEnumerable.js @@ -0,0 +1,59 @@ +import {newObjectWithoutPrototypeProperties} from './newObjectWithoutPrototypeProperties'; + +const dangerousProperties = newObjectWithoutPrototypeProperties(); + +getFunctionPropertiesOf(Object.prototype).forEach(propertyName => { + dangerousProperties[propertyName] = true; +}); +getFunctionPropertiesOf(Array.prototype).forEach(propertyName => { + dangerousProperties[propertyName] = true; +}); +getFunctionPropertiesOf(Function.prototype).forEach(propertyName => { + dangerousProperties[propertyName] = true; +}); +getFunctionPropertiesOf(String.prototype).forEach(propertyName => { + dangerousProperties[propertyName] = true; +}); + +// eslint-disable-next-line no-proto +dangerousProperties.__proto__ = true; +dangerousProperties.__defineGetter__ = true; +dangerousProperties.__defineSetter__ = true; + +// Following properties are not _that_ dangerous +delete dangerousProperties.toString; +delete dangerousProperties.includes; +delete dangerousProperties.slice; +delete dangerousProperties.isPrototypeOf; +delete dangerousProperties.propertyIsEnumerable; +delete dangerousProperties.indexOf; +delete dangerousProperties.keys; +delete dangerousProperties.lastIndexOf; + +function getFunctionPropertiesOf(obj) { + return Object.getOwnPropertyNames(obj) + .filter(propertyName => { + try { + return typeof obj[propertyName] === 'function'; + } catch (error) { + // TypeError happens here when accessing 'caller', 'callee', 'arguments' on Function + return true; + } + }); +} + +/** + * Checks if a property can be harmful and should only processed when it is enumerable on its parent. + * + * This is necessary because of various "arbitrary-code-execution" issues that Handlebars has faced in the past. + * + * @param propertyName + * @returns {boolean} + */ +export function propertyMustBeEnumerable(propertyName) { + return dangerousProperties[propertyName]; +} + +export function getAllDangerousProperties() { + return dangerousProperties; +} diff --git a/lib/handlebars/internal/utils.js b/lib/handlebars/internal/utils.js deleted file mode 100644 index 00251fe47..000000000 --- a/lib/handlebars/internal/utils.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Create a new object with `null`-constructor containing all enumerable properties of all source objects. - * - * Intention: omit potentially malicious prototype properties - * - * @private - * @param {...object} sourceObjects - * @returns {object} - */ -export function newMapObject(...sourceObjects) { - let result = Object.create(null); - sourceObjects.forEach(sourceObject => { - if (sourceObject != null) { - Object.keys(sourceObject).forEach(key => { - result[key] = sourceObject[key]; - }); - } - }); - return result; -} diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index d2e710806..c2836934c 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -2,7 +2,7 @@ import * as Utils from './utils'; import Exception from './exception'; import {COMPILER_REVISION, createFrame, LAST_COMPATIBLE_COMPILER_REVISION, REVISION_CHANGES} from './base'; import {moveHelperToHooks} from './helpers'; -import {newMapObject} from './internal/utils'; +import {newObjectWithoutPrototypeProperties} from './internal/newObjectWithoutPrototypeProperties'; export function checkRevision(compilerInfo) { const compilerRevision = compilerInfo && compilerInfo[0] || 1, @@ -159,13 +159,13 @@ export function template(templateSpec, env) { ret._setup = function(options) { if (!options.partial) { - container.helpers = newMapObject(env.helpers, options.helpers); + container.helpers = newObjectWithoutPrototypeProperties(env.helpers, options.helpers); if (templateSpec.usePartial) { - container.partials = newMapObject(env.partials, options.partials); + container.partials = newObjectWithoutPrototypeProperties(env.partials, options.partials); } if (templateSpec.usePartial || templateSpec.useDecorators) { - container.decorators = newMapObject(env.decorators, options.decorators); + container.decorators = newObjectWithoutPrototypeProperties(env.decorators, options.decorators); } container.hooks = {}; diff --git a/types/index.d.ts b/types/index.d.ts index 8a24eb1dc..d8e64307a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -215,11 +215,6 @@ interface CompileOptions { ignoreStandalone?: boolean; explicitPartialContext?: boolean; allowNonHelperFunctionCall?: boolean; - propertyMustBeEnumerable?: PropertyMustBeEnumerable; -} - -type PropertyMustBeEnumerable = { - [name: string]: boolean; } type KnownHelpers = {