Skip to content

Commit

Permalink
feat: default options for controlling proto access
Browse files Browse the repository at this point in the history
This commmit adds the runtime options
- `allowProtoPropertiesByDefault` (boolean, default: false) and
- `allowProtoMethodsByDefault` (boolean, default: false)`
which can be used to allow access to prototype properties and
functions in general.

Specific properties and methods can still be disabled from access
via `allowedProtoProperties` and `allowedProtoMethods` by
setting the corresponding values to false.

The methods `constructor`, `__defineGetter__`, `__defineSetter__`, `__lookupGetter__`
and the property `__proto__` will be disabled, even if the allow...ByDefault-options
are set to true. In order to allow access to those properties and methods, they have
to be explicitly set to true in the 'allowedProto...'-options.

A warning is logged when the a proto-access it attempted and denied
by default (i.e. if no option is set by the user to make the access
decision explicit)
  • Loading branch information
nknapp committed Jan 10, 2020
1 parent 91a1b5d commit f6d29eb
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 121 deletions.
55 changes: 55 additions & 0 deletions lib/handlebars/internal/proto-access.js
@@ -0,0 +1,55 @@
import { createNewLookupObject } from './create-new-lookup-object';

export function createProtoAccessControl(runtimeOptions) {
let defaultMethodWhiteList = Object.create(null);
defaultMethodWhiteList['constructor'] = false;
defaultMethodWhiteList['__defineGetter__'] = false;
defaultMethodWhiteList['__defineSetter__'] = false;
defaultMethodWhiteList['__lookupGetter__'] = false;

let defaultPropertyWhiteList = Object.create(null);
// eslint-disable-next-line no-proto
defaultPropertyWhiteList['__proto__'] = false;

return {
properties: {
whitelist: createNewLookupObject(
defaultPropertyWhiteList,
runtimeOptions.allowedProtoProperties
),
defaultValue: runtimeOptions.allowProtoPropertiesByDefault
},
methods: {
whitelist: createNewLookupObject(
defaultMethodWhiteList,
runtimeOptions.allowedProtoMethods
),
defaultValue: runtimeOptions.allowProtoMethodsByDefault
}
};
}

export function resultIsAllowed(result, protoAccessControl, propertyName) {
if (typeof result === 'function') {
return checkWhiteList(protoAccessControl.methods, propertyName);
} else {
return checkWhiteList(protoAccessControl.properties, propertyName);
}
}

function checkWhiteList(protoAccessControlForType, propertyName) {
if (protoAccessControlForType.whitelist[propertyName] !== undefined) {
return protoAccessControlForType.whitelist[propertyName] === true;
}
if (protoAccessControlForType.defaultValue !== undefined) {
return protoAccessControlForType.defaultValue;
}

// eslint-disable-next-line no-console
console.error(
`Handlebars: Access has been denied to resolve the property "${propertyName}" because it is not an "own property" of its parent.\n` +
`You can add a runtime option to disable the check or this warning:\n` +
`See http://localhost:8080/api-reference/runtime-options.html#options-to-control-prototype-access for details`
);
return false;
}
29 changes: 13 additions & 16 deletions lib/handlebars/runtime.js
Expand Up @@ -8,7 +8,10 @@ import {
} from './base';
import { moveHelperToHooks } from './helpers';
import { wrapHelper } from './internal/wrapHelper';
import { createNewLookupObject } from './internal/createNewLookupObject';
import {
createProtoAccessControl,
resultIsAllowed
} from './internal/proto-access';

export function checkRevision(compilerInfo) {
const compilerRevision = (compilerInfo && compilerInfo[0]) || 1,
Expand Down Expand Up @@ -73,8 +76,7 @@ export function template(templateSpec, env) {

let extendedOptions = Utils.extend({}, options, {
hooks: this.hooks,
allowedProtoMethods: this.allowedProtoMethods,
allowedProtoProperties: this.allowedProtoProperties
protoAccessControl: this.protoAccessControl
});

let result = env.VM.invokePartial.call(
Expand Down Expand Up @@ -126,15 +128,14 @@ export function template(templateSpec, env) {
},
lookupProperty: function(parent, propertyName) {
let result = parent[propertyName];
if (result == null) {
return result;
}
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return result;
}
const whitelist =
typeof result === 'function'
? container.allowedProtoMethods
: container.allowedProtoProperties;

if (whitelist[propertyName] === true) {
if (resultIsAllowed(result, container.protoAccessControl, propertyName)) {
return result;
}
return undefined;
Expand Down Expand Up @@ -237,6 +238,7 @@ export function template(templateSpec, env) {
)
);
}

main = executeDecorators(
templateSpec.main,
main,
Expand All @@ -247,6 +249,7 @@ export function template(templateSpec, env) {
);
return main(context, options);
}

ret.isTop = true;

ret._setup = function(options) {
Expand All @@ -271,21 +274,15 @@ export function template(templateSpec, env) {
}

container.hooks = {};
container.allowedProtoProperties = createNewLookupObject(
options.allowedProtoProperties
);
container.allowedProtoMethods = createNewLookupObject(
options.allowedProtoMethods
);
container.protoAccessControl = createProtoAccessControl(options);

let keepHelperInHelpers =
options.allowCallsToHelperMissing ||
templateWasPrecompiledWithCompilerV7;
moveHelperToHooks(container, 'helperMissing', keepHelperInHelpers);
moveHelperToHooks(container, 'blockHelperMissing', keepHelperInHelpers);
} else {
container.allowedProtoProperties = options.allowedProtoProperties;
container.allowedProtoMethods = options.allowedProtoMethods;
container.protoAccessControl = options.protoAccessControl; // internal option
container.helpers = options.helpers;
container.partials = options.partials;
container.decorators = options.decorators;
Expand Down

0 comments on commit f6d29eb

Please sign in to comment.