mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-16 21:00:40 -07:00
refactor: trim scope-security helpers and docs
Inline findScope and blocked-key checks, shorten ownPropertyOnly docs, and drop implementation-detail push() unit tests. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -138,7 +138,7 @@ It defaults to `false`. For example, when set to `true`, a blank string would ev
|
||||
|
||||
**lenientIf** modifies the behavior of `strictVariables` to allow handling optional variables. If set to `true`, an undefined variable will *not* cause an exception in the following two situations: a) it is the condition to an `if`, `elsif`, or `unless` tag; b) it occurs right before a `default` filter. Irrelevant if `strictVariables` is not set. Defaults to `false`.
|
||||
|
||||
**ownPropertyOnly** limits 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 `ownPropertyOnly` is `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. [`Drop`][drop] values, iteration, `.size`/`.first`/`.last`, filters, and custom tags follow separate rules—see [Security Model](./security-model.html).
|
||||
**ownPropertyOnly** limits template property reads on plain scope objects to own properties (no inherited prototype keys). Defaults to `true`. Proto keys (`__proto__`, `constructor`, `prototype`) are blocked when `true`. See [Security Model](./security-model.html).
|
||||
|
||||
{% note info Nonexistent Tags %}
|
||||
Nonexistent tags always throw errors during parsing and this behavior cannot be customized.
|
||||
@@ -161,4 +161,3 @@ Parameter orders are ignored by default, for example `{% for i in (1..8) reverse
|
||||
[jekyllInclude]: /api/interfaces/LiquidOptions.html#jekyllInclude
|
||||
[raw]: ../filters/raw.html
|
||||
[outputEscape]: /api/interfaces/LiquidOptions.html#outputEscape
|
||||
[drop]: /api/classes/Drop.html
|
||||
|
||||
@@ -50,18 +50,11 @@ The `memoryLimit` option was removed in v11; enforce memory limits at the host o
|
||||
|
||||
## `ownPropertyOnly` and scope data
|
||||
|
||||
[`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.
|
||||
With [`ownPropertyOnly`][ownPropertyOnly] `true` (default), plain scope objects only expose **own** properties (no inherited / `Object.prototype` keys). Proto keys (`__proto__`, `constructor`, `prototype`) are blocked when `true`, even as own properties; when `false`, own properties with those names are allowed but inherited access to those names is still blocked.
|
||||
|
||||
**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).
|
||||
Not restricted: [`Drop`][drop] values, iteration via `Symbol.iterator`, `.size`/`.first`/`.last`, filters, and custom tags.
|
||||
|
||||
**Exceptions** — `ownPropertyOnly` does not restrict:
|
||||
|
||||
- [`Drop`][drop] values: prototype chain and [`liquidMethodMissing`][liquidMethodMissing] still apply; audit custom drops like privileged code.
|
||||
- Iteration (`{% for %}`, `{% tablerow %}`, `{% render for %}`): uses `Symbol.iterator` when present, including inherited iterators on plain objects; class instances and drops keep their iterators too.
|
||||
- Liquid pseudo-properties `.size`, `.first`, and `.last`: arrays and strings use length/index rules; `Map`/`Set` use their native size; plain objects with an own `size` property use that value (inherited `size` on plain objects is ignored when `ownPropertyOnly` is `true`).
|
||||
- Filters and custom tags: operate on resolved values with their own semantics.
|
||||
|
||||
Use `true` for untrusted or polluted objects; add [`strictVariables`][strictVariables] if missing paths should error. Override per render via [`RenderOptions`][renderOwnPropertyOnly]. For deeply untrusted input, pre-sanitize scope objects before `render()` (for example with [@hapi/bourne](https://www.npmjs.com/package/@hapi/bourne)). This is a read policy for scope data—not a sandbox for filters, tags, or your code.
|
||||
Use `true` for untrusted objects; add [`strictVariables`][strictVariables] if missing paths should error. Override per render via [`RenderOptions`][renderOwnPropertyOnly]. This is a read policy for scope data—not a sandbox for filters, tags, or your code.
|
||||
|
||||
## Custom `Drop` classes
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Context } from './context'
|
||||
import { Drop } from '../drop/drop'
|
||||
import { Scope } from './scope'
|
||||
|
||||
describe('Context', function () {
|
||||
@@ -251,21 +250,11 @@ describe('Context', function () {
|
||||
expect(ctx.getSync(['bar', 'foo'])).toEqual('foo')
|
||||
expect(ctx.getSync(['bar', 'bar'])).toEqual(undefined)
|
||||
})
|
||||
it('should wrap plain objects with null prototype', function () {
|
||||
const scope = ctx.push({ foo: 'FOO' })
|
||||
expect(Object.getPrototypeOf(scope)).toBeNull()
|
||||
})
|
||||
it('should return pushed scope for in-place mutation', function () {
|
||||
const scope = ctx.push({})
|
||||
scope.item = 'ITEM'
|
||||
expect(ctx.getSync(['item'])).toEqual('ITEM')
|
||||
})
|
||||
it('should push Drop instances as-is', function () {
|
||||
class TestDrop extends Drop {}
|
||||
const drop = new TestDrop()
|
||||
const pushed = ctx.push(drop)
|
||||
expect(pushed).toBe(drop)
|
||||
})
|
||||
})
|
||||
describe('.pop()', function () {
|
||||
it('should pop scope', async function () {
|
||||
|
||||
+5
-15
@@ -118,17 +118,11 @@ export class Context {
|
||||
})
|
||||
}
|
||||
private findScope (key: string | number) {
|
||||
const hasKey = (obj: Scope) => {
|
||||
if (obj == null) return false
|
||||
return this.ownPropertyOnly
|
||||
? hasOwnProperty.call(obj, key)
|
||||
: key in obj
|
||||
}
|
||||
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
||||
const candidate = this.scopes[i]
|
||||
if (hasKey(candidate)) return candidate
|
||||
if (this.ownPropertyOnly ? hasOwnProperty.call(candidate, key) : key in candidate) return candidate
|
||||
}
|
||||
if (hasKey(this.environments)) return this.environments
|
||||
if (this.ownPropertyOnly ? hasOwnProperty.call(this.environments, key) : key in this.environments) return this.environments
|
||||
return this.globals
|
||||
}
|
||||
readProperty (obj: Scope, key: (PropertyKey | Drop)) {
|
||||
@@ -163,14 +157,10 @@ 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 (typeof key === 'string' && BLOCKED_SCOPE_KEYS.has(key)) {
|
||||
if (ownPropertyOnly || !hasOwnProperty.call(obj, key)) return undefined
|
||||
}
|
||||
if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop)) return undefined
|
||||
return obj[key]
|
||||
}
|
||||
|
||||
@@ -38,11 +38,7 @@ export interface LiquidOptions {
|
||||
strictVariables?: boolean;
|
||||
/** Catch all errors instead of exit upon one. Please note that render errors won't be reached when parse fails. */
|
||||
catchAllErrors?: boolean;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/** Limit template property reads on plain scope objects to own properties. Defaults to `true`. Proto keys (`__proto__`, `constructor`, `prototype`) are blocked when `true`. */
|
||||
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`. **/
|
||||
lenientIf?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user