mirror of
https://github.com/harttle/liquidjs.git
synced 2026-10-02 16:45:14 -07:00
refactor: drop redundant tag write-path blocking
Write blocking on assign/capture/increment/decrement duplicated read-side protection in readJSProperty; null-proto scopes from push already prevent prototype pollution on managed writes. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -52,7 +52,7 @@ The `memoryLimit` option was removed in v11; enforce memory limits at the host o
|
|||||||
|
|
||||||
[`ownPropertyOnly`][ownPropertyOnly] controls **template property reads on plain scope objects** (objects whose prototype is `null` or `Object.prototype`). Default `true`. When enabled, only own enumerable properties are visible to variable lookup; inherited keys from `Object.prototype` or other prototypes are hidden.
|
[`ownPropertyOnly`][ownPropertyOnly] controls **template property reads on plain scope objects** (objects whose prototype is `null` or `Object.prototype`). Default `true`. When enabled, only own enumerable properties are visible to variable lookup; inherited keys from `Object.prototype` or other prototypes are hidden.
|
||||||
|
|
||||||
**Proto-related keys** (`__proto__`, `constructor`, `prototype`): when [`ownPropertyOnly`][ownPropertyOnly] is `true` (default), template reads and writes to those names are blocked even if they are own properties—this defends against prototype pollution from sources such as `JSON.parse('{"__proto__":…}')`. When `ownPropertyOnly` is `false`, own properties with those names are allowed; inherited prototype-chain access to those names is still blocked. Managed scopes use null prototypes (loop locals, `{% render %}` bindings, filter iteration scopes).
|
**Proto-related keys** (`__proto__`, `constructor`, `prototype`): when [`ownPropertyOnly`][ownPropertyOnly] is `true` (default), template reads of those names are blocked even if they are own properties—this defends against prototype pollution from sources such as `JSON.parse('{"__proto__":…}')`. When `ownPropertyOnly` is `false`, own properties with those names are allowed; inherited prototype-chain access to those names is still blocked. Managed scopes use null prototypes (loop locals, `{% render %}` bindings, filter iteration scopes).
|
||||||
|
|
||||||
**Exceptions** — `ownPropertyOnly` does not restrict:
|
**Exceptions** — `ownPropertyOnly` does not restrict:
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type Scope = ScopeObject | Drop
|
|||||||
|
|
||||||
const BLOCKED_SCOPE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
const BLOCKED_SCOPE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
||||||
|
|
||||||
export function isBlockedScopeKey (key: PropertyKey): boolean {
|
function isBlockedScopeKey (key: PropertyKey): boolean {
|
||||||
return typeof key === 'string' && BLOCKED_SCOPE_KEYS.has(key)
|
return typeof key === 'string' && BLOCKED_SCOPE_KEYS.has(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,10 +19,6 @@ export function shouldBlockScopeKeyRead (obj: Scope, key: PropertyKey, ownProper
|
|||||||
return !hasOwnProperty.call(obj, key)
|
return !hasOwnProperty.call(obj, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldBlockScopeKeyWrite (key: PropertyKey, ownPropertyOnly: boolean): boolean {
|
|
||||||
return ownPropertyOnly && isBlockedScopeKey(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createScope (from?: ScopeObject): ScopeObject {
|
export function createScope (from?: ScopeObject): ScopeObject {
|
||||||
return Object.assign(Object.create(null), from)
|
return Object.assign(Object.create(null), from)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Value, Liquid, TopLevelToken, TagToken, Context, Tag } from '..'
|
import { Value, Liquid, TopLevelToken, TagToken, Context, Tag } from '..'
|
||||||
import { shouldBlockScopeKeyWrite } from '../context/scope'
|
|
||||||
import { Arguments } from '../template'
|
import { Arguments } from '../template'
|
||||||
import { IdentifierToken } from '../tokens'
|
import { IdentifierToken } from '../tokens'
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ export default class extends Tag {
|
|||||||
this.value = new Value(this.tokenizer.readFilteredValue(), this.liquid)
|
this.value = new Value(this.tokenizer.readFilteredValue(), this.liquid)
|
||||||
}
|
}
|
||||||
* render (ctx: Context): Generator<unknown, void, unknown> {
|
* render (ctx: Context): Generator<unknown, void, unknown> {
|
||||||
if (shouldBlockScopeKeyWrite(this.key, ctx.ownPropertyOnly)) return
|
|
||||||
ctx.bottom()[this.key] = yield this.value.value(ctx, this.liquid.options.lenientIf)
|
ctx.bottom()[this.key] = yield this.value.value(ctx, this.liquid.options.lenientIf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Liquid, Tag, Template, Context, TagToken, TopLevelToken } from '..'
|
import { Liquid, Tag, Template, Context, TagToken, TopLevelToken } from '..'
|
||||||
import { shouldBlockScopeKeyWrite } from '../context/scope'
|
|
||||||
import { Parser } from '../parser'
|
import { Parser } from '../parser'
|
||||||
import { IdentifierToken, QuotedToken } from '../tokens'
|
import { IdentifierToken, QuotedToken } from '../tokens'
|
||||||
import { isTagToken } from '../util'
|
import { isTagToken } from '../util'
|
||||||
@@ -32,7 +31,6 @@ export default class extends Tag {
|
|||||||
* render (ctx: Context): Generator<unknown, void, string> {
|
* render (ctx: Context): Generator<unknown, void, string> {
|
||||||
const r = this.liquid.renderer
|
const r = this.liquid.renderer
|
||||||
const html = yield r.renderTemplates(this.templates, ctx)
|
const html = yield r.renderTemplates(this.templates, ctx)
|
||||||
if (shouldBlockScopeKeyWrite(this.variable, ctx.ownPropertyOnly)) return
|
|
||||||
ctx.bottom()[this.variable] = html
|
ctx.bottom()[this.variable] = html
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Tag, Liquid, TopLevelToken, Emitter, TagToken, Context } from '..'
|
import { Tag, Liquid, TopLevelToken, Emitter, TagToken, Context } from '..'
|
||||||
import { shouldBlockScopeKeyWrite } from '../context/scope'
|
|
||||||
import { IdentifierToken } from '../tokens'
|
import { IdentifierToken } from '../tokens'
|
||||||
import { isNumber, stringify } from '../util'
|
import { isNumber, stringify } from '../util'
|
||||||
|
|
||||||
@@ -12,7 +11,6 @@ export default class extends Tag {
|
|||||||
this.variable = this.identifier.content
|
this.variable = this.identifier.content
|
||||||
}
|
}
|
||||||
render (context: Context, emitter: Emitter) {
|
render (context: Context, emitter: Emitter) {
|
||||||
if (shouldBlockScopeKeyWrite(this.variable, context.ownPropertyOnly)) return
|
|
||||||
const scope = context.environments
|
const scope = context.environments
|
||||||
if (!isNumber(scope[this.variable])) {
|
if (!isNumber(scope[this.variable])) {
|
||||||
scope[this.variable] = 0
|
scope[this.variable] = 0
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { isNumber, stringify } from '../util'
|
import { isNumber, stringify } from '../util'
|
||||||
import { shouldBlockScopeKeyWrite } from '../context/scope'
|
|
||||||
import { Tag, Liquid, TopLevelToken, Emitter, TagToken, Context } from '..'
|
import { Tag, Liquid, TopLevelToken, Emitter, TagToken, Context } from '..'
|
||||||
import { IdentifierToken } from '../tokens'
|
import { IdentifierToken } from '../tokens'
|
||||||
|
|
||||||
@@ -12,7 +11,6 @@ export default class extends Tag {
|
|||||||
this.variable = this.identifier.content
|
this.variable = this.identifier.content
|
||||||
}
|
}
|
||||||
render (context: Context, emitter: Emitter) {
|
render (context: Context, emitter: Emitter) {
|
||||||
if (shouldBlockScopeKeyWrite(this.variable, context.ownPropertyOnly)) return
|
|
||||||
const scope = context.environments
|
const scope = context.environments
|
||||||
if (!isNumber(scope[this.variable])) {
|
if (!isNumber(scope[this.variable])) {
|
||||||
scope[this.variable] = 0
|
scope[this.variable] = 0
|
||||||
|
|||||||
@@ -19,25 +19,11 @@ describe('scope security', function () {
|
|||||||
await expect(liquid.parseAndRender('{{ constructor.name }}', scope)).resolves.toBe('')
|
await expect(liquid.parseAndRender('{{ constructor.name }}', scope)).resolves.toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should block assign to __proto__', async function () {
|
|
||||||
await expect(liquid.parseAndRender(
|
|
||||||
'{% assign __proto__ = obj %}{{ __proto__.polluted }}',
|
|
||||||
{ obj: { polluted: true } }
|
|
||||||
)).resolves.toBe('')
|
|
||||||
expect((Object.prototype as any).polluted).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should block inherited constructor when ownPropertyOnly=false', async function () {
|
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('{{ foo.constructor.name }}', { foo: {} }, { ownPropertyOnly: false })).resolves.toBe('')
|
||||||
await expect(liquid.parseAndRender('{{ constructor.name }}', { name: 'Alice' }, { ownPropertyOnly: false })).resolves.toBe('')
|
await expect(liquid.parseAndRender('{{ constructor.name }}', { name: 'Alice' }, { ownPropertyOnly: false })).resolves.toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not write increment to __proto__ on user scope', async function () {
|
|
||||||
const scope = Object.create(null) as Record<string, unknown>
|
|
||||||
await expect(liquid.parseAndRender('{% increment __proto__ %}', scope)).resolves.toBe('')
|
|
||||||
expect(scope).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should iterate plain objects via inherited Symbol.iterator (ownPropertyOnly exception)', async function () {
|
it('should iterate plain objects via inherited Symbol.iterator (ownPropertyOnly exception)', async function () {
|
||||||
// eslint-disable-next-line no-extend-native
|
// eslint-disable-next-line no-extend-native
|
||||||
(Object.prototype as any)[Symbol.iterator] = function * () { yield 'inherited' }
|
(Object.prototype as any)[Symbol.iterator] = function * () { yield 'inherited' }
|
||||||
|
|||||||
Reference in New Issue
Block a user