Safe JSON Path

Checks the keys, pointers and path expressions that reach a JSON column or document store. A path is a query, so operator input in one is the same class of problem as operator input in SQL.

The checks refuse what a renderer or a store would read differently from how it looks: NUL and control characters, bidirectional overrides and zero-width characters, all of which let a key print as one thing and match another. Keys, pointer depth and expression length are bounded, so a path cannot become the denial of service.

A JSONPath filter expression is refused outright rather than sanitized. A filter carries predicate logic, and there is no reliable way to accept one built from operator input: the path is built at the call site with bound values instead.

b.safeJsonPath.validateKey(key, opts?) #

stable0.8.44
{
  maxBytes: number,   // default: the module's MAX_KEY_BYTES
}

Check one JSON object key and return it unchanged.

A non-string raises safe-jsonpath/bad-key, as does an empty one. Over the byte limit raises safe-jsonpath/key-too-long, and a key carrying NUL, control, bidirectional or zero-width characters raises safe-jsonpath/key-control-char, because such a key displays as one thing in a log or a UI and matches another in the store.

The key is returned rather than rewritten: a key is an identifier, and silently changing one would address a different field than the caller asked for.

var b = require("@blamejs/core");
b.safeJsonPath.validateKey("patientId");     // → "patientId"

b.safeJsonPath.validatePointer(pointer, opts?) #

stable0.8.44
{
  maxSegments: number,   // default: the module's MAX_POINTER_SEGMENTS
  maxBytes:    number,   // per-key byte limit
}

Check a pointer given as an array of segments and return it unchanged. A string segment is a key and goes through validateKey; a numeric segment is an array index and has to be a non-negative integer.

A pointer that is not an array raises safe-jsonpath/bad-pointer, and one longer than the segment limit raises safe-jsonpath/pointer-too-long, since depth is what turns a lookup into work proportional to the document. A negative or fractional index raises safe-jsonpath/pointer-bad-index, and anything that is neither a string nor a number raises safe-jsonpath/pointer-bad-segment.

A string segment carries the key checks with it: safe-jsonpath/bad-key for an empty one, safe-jsonpath/key-too-long over the byte limit, and safe-jsonpath/key-control-char for NUL, control, bidirectional or zero-width characters.

Taking the pointer as an array rather than a string is the point: there is no separator to escape, so a key containing a dot or a slash cannot become two segments.

var b = require("@blamejs/core");
b.safeJsonPath.validatePointer(["patients", 0, "name"]);

b.safeJsonPath.validateExpression(expr, opts?) #

stable0.8.44
{
  maxBytes: number,   // default: the module's MAX_EXPRESSION_BYTES
  maxDepth: number,   // bracket-nesting ceiling; default 8
}

Check a JSONPath expression string and return it unchanged.

A filter expression, ?(...), is refused with safe-jsonpath/filter-expr-refused whatever it contains. A filter is predicate logic, and a validator cannot tell a safe one built from operator input from an unsafe one, so the answer is to build the path at the call site with bound values.

Three more constructs are refused outright for the same reason. A deep scan, $.., raises safe-jsonpath/deep-scan-refused: it multiplies traversal cost and ignores whatever shape the caller assumed. A script-shaped (@.x…) raises safe-jsonpath/script-expr-refused, because evaluators that route paths through dynamic code turn that into remote code execution. A JavaScript-source hint anywhere in the expression raises safe-jsonpath/dynamic-hint-refused.

The remaining refusals are safe-jsonpath/bad-expression for a non-string or empty expression, safe-jsonpath/expression-too-long over the byte limit, safe-jsonpath/expression-control-char for NUL, control, bidirectional or zero-width characters, and safe-jsonpath/expression-too-deep when bracket nesting passes maxDepth.

var b = require("@blamejs/core");
b.safeJsonPath.validateExpression("$.patients[0].name");
try { b.safeJsonPath.validateExpression("$.p[?(@.x=='y')]"); }
catch (e) { e.code; }        // → "safe-jsonpath/filter-expr-refused"

b.safeJsonPath.validateContainment(value, opts?) #

stable0.8.44
{
  maxDepth: number,   // nesting ceiling
  maxNodes: number,   // total nodes walked; default 1024
}

Check a containment shape, the object a JSON column is queried by, and return it unchanged.

It walks the whole shape and bounds both dimensions: more than maxNodes raises safe-jsonpath/containment-too-large and nesting past maxDepth raises safe-jsonpath/containment-too-deep. A shape is operator input, and a wide or deep one costs the store the walk whether or not it matches anything.

Every key goes through the same check as a standalone key, so safe-jsonpath/bad-key, safe-jsonpath/key-too-long and safe-jsonpath/key-control-char reach the caller from here too. A string leaf carrying control, bidirectional or zero-width characters raises safe-jsonpath/containment-bad-string, and one longer than the key limit in characters raises safe-jsonpath/containment-string-too-long, so a shape cannot match on a value that displays as something else. A leaf that is not a JSON type at all, a function or a symbol, raises safe-jsonpath/containment-bad-type.

var b = require("@blamejs/core");
b.safeJsonPath.validateContainment({ status: "active", tags: ["a", "b"] });

Last updated 2026-10-12T14:21:56.879Z by seeder.