diff --git a/src/context/context.ts b/src/context/context.ts index 957579794..a7caf8e56 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -1,7 +1,7 @@ import { Drop } from '../drop/drop' import { __assign } from 'tslib' import { NormalizedFullOptions, defaultOptions, RenderOptions } from '../liquid-options' -import { createScope, Scope, shouldBlockScopeKeyRead } from './scope' +import { createScope, Scope } from './scope' import { hasOwnProperty, isArray, isNil, isUndefined, isString, isFunction, isNumber, toLiquid, InternalUndefinedVariableError, toValueSync, isObject, Limiter, toValue, readArrayElement } from '../util' type PropertyKey = string | number; @@ -165,6 +165,14 @@ export class Context { } } +const BLOCKED_SCOPE_KEYS = new Set(['__proto__', 'constructor', 'prototype']) + +function shouldBlockScopeKeyRead (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean): boolean { + if (typeof key !== 'string' || !BLOCKED_SCOPE_KEYS.has(key)) return false + if (ownPropertyOnly) return true + return !hasOwnProperty.call(obj, key) +} + export function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) { if (shouldBlockScopeKeyRead(obj, key, ownPropertyOnly)) return undefined if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop)) return undefined diff --git a/src/context/scope.ts b/src/context/scope.ts index 9ae157d20..e5fd79943 100644 --- a/src/context/scope.ts +++ b/src/context/scope.ts @@ -1,5 +1,4 @@ import { Drop } from '../drop/drop' -import { hasOwnProperty } from '../util' export interface ScopeObject extends Record { toLiquid?: () => any; @@ -7,18 +6,6 @@ export interface ScopeObject extends Record { export type Scope = ScopeObject | Drop -const BLOCKED_SCOPE_KEYS = new Set(['__proto__', 'constructor', 'prototype']) - -function isBlockedScopeKey (key: PropertyKey): boolean { - return typeof key === 'string' && BLOCKED_SCOPE_KEYS.has(key) -} - -export function shouldBlockScopeKeyRead (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean): boolean { - if (!isBlockedScopeKey(key)) return false - if (ownPropertyOnly) return true - return !hasOwnProperty.call(obj, key) -} - export function createScope (from?: ScopeObject): ScopeObject { return Object.assign(Object.create(null), from) } diff --git a/src/liquid-options.ts b/src/liquid-options.ts index f0fc1bff6..2467755e8 100644 --- a/src/liquid-options.ts +++ b/src/liquid-options.ts @@ -39,8 +39,9 @@ export interface LiquidOptions { /** Catch all errors instead of exit upon one. Please note that render errors won't be reached when parse fails. */ catchAllErrors?: boolean; /** - * Hide scope variables from prototypes, useful when you're passing a not sanitized object into LiquidJS or need to hide prototypes from templates. - * This only applies to property/index access on scope objects. Filter transforms and iteration operate on the resolved value with standard JavaScript semantics, so prototype-inherited array indices may still be surfaced by them. + * Limit template property reads on plain scope objects to own properties (no inherited prototype keys). Defaults to `true`. + * Proto-related keys (`__proto__`, `constructor`, `prototype`) are blocked when `true` (even as own properties); when `false`, own properties with those names are allowed and inherited prototype-chain access to those names is still blocked. + * Drops, iteration, `.size`/`.first`/`.last`, filters, and custom tags follow separate rules. */ ownPropertyOnly?: boolean; /** Modifies the behavior of `strictVariables`. If set, a single undefined variable will *not* cause an exception in the context of the `if`/`elsif`/`unless` tag and the `default` filter. Instead, it will evaluate to `false` and `null`, respectively. Irrelevant if `strictVariables` is not set. Defaults to `false`. **/ diff --git a/src/tags/include.ts b/src/tags/include.ts index a0965a617..8c7e9d228 100644 --- a/src/tags/include.ts +++ b/src/tags/include.ts @@ -1,5 +1,5 @@ import { Template, ValueToken, TopLevelToken, Liquid, Tag, assert, evalToken, Hash, Emitter, TagToken, Context } from '..' -import { BlockMode, Scope } from '../context' +import { BlockMode, Scope, createScope } from '../context' import { Parser } from '../parser' import { Argument, Arguments, PartialScope } from '../template' import { isString, isValueToken } from '../util' @@ -40,7 +40,7 @@ export default class extends Tag { const scope = (yield hash.render(ctx)) as Scope if (withVar) scope[filepath] = yield evalToken(withVar, ctx) const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this.currentFile)) as Template[] - ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope) + ctx.push(ctx.opts.jekyllInclude ? { include: createScope(scope) } : scope) yield renderer.renderTemplates(templates, ctx, emitter) ctx.pop() ctx.restoreRegister(saved) diff --git a/test/integration/liquid/scope-security.spec.ts b/test/integration/liquid/scope-security.spec.ts index 6bb6a483f..2c3ae8973 100644 --- a/test/integration/liquid/scope-security.spec.ts +++ b/test/integration/liquid/scope-security.spec.ts @@ -8,22 +8,6 @@ describe('scope security', function () { liquid = new Liquid() }) - it('should not read __proto__ from passed scope', async function () { - const scope = JSON.parse('{"__proto__": {"polluted": true}, "name": "Alice"}') - await expect(liquid.parseAndRender('{{ name }}', scope)).resolves.toBe('Alice') - await expect(liquid.parseAndRender('{{ __proto__.polluted }}', scope)).resolves.toBe('') - }) - - it('should not read constructor from passed scope', async function () { - const scope = { name: 'Alice', constructor: { name: 'Object' } } - await expect(liquid.parseAndRender('{{ constructor.name }}', scope)).resolves.toBe('') - }) - - it('should block inherited constructor when ownPropertyOnly=false', async function () { - await expect(liquid.parseAndRender('{{ foo.constructor.name }}', { foo: {} }, { ownPropertyOnly: false })).resolves.toBe('') - await expect(liquid.parseAndRender('{{ constructor.name }}', { name: 'Alice' }, { ownPropertyOnly: false })).resolves.toBe('') - }) - it('should iterate plain objects via inherited Symbol.iterator (ownPropertyOnly exception)', async function () { // eslint-disable-next-line no-extend-native (Object.prototype as any)[Symbol.iterator] = function * () { yield 'inherited' }