Skip to content

Security: Alexis-Technologies/metaschema

Security

SECURITY.md

Security Policy

Supported versions

Only the latest published version receives fixes.

Version Supported
3.x ✅
2.x ❌ (upgrade: the CHANGELOG has the notes)
1.x ❌
metaschema (metarhia) 2.x ❌ (report upstream)

Reporting a vulnerability

Please do not open a public issue for a security problem.

Report it privately through GitHub's private vulnerability reporting, or by email to dolid.sasha@gmail.com.

Include, as far as you can:

  • what an attacker can do, and what they need in order to do it;
  • the affected version and Node.js version (or browser);
  • a minimal reproduction.

You can expect an acknowledgement within a few days, and an assessment of severity and a fix timeline once the report is confirmed.

Threat model

Some things work as intended and are not vulnerabilities:

  • Schemas are code. A schema is a JavaScript value supplied by the application. Field and schema validate functions, custom types' construct / checkType, calculated fields, the parse and serialize functions of a field and of a schema (decoders and serializers), and default factories are live functions that run with the privileges of your process; a decoder's exception is an issue, a serializer's or a default factory's exception is yours. A getter on the value under check is the program's code too, and an exception from it leaves check. Build schemas only from definitions you trust, the same as any other module you require.
  • metaschema never loads or evaluates code it did not write. Since 1.0 it has no loader: it does not read schema files, evaluate source strings or run a sandbox. Turning untrusted input into a schema definition is the application's decision and responsibility. The one place that builds a function from source is the @alexify/metaschema/compile subpath (3.0), and only when the application calls its compile() or register(): the source is generated by the package from the schema's plan, nothing taken from a schema or from a value under check reaches it except as a JSON string literal (regular expressions, enum values, functions and nested schemas are passed by reference through a scope), the generated code is strict and named in a sourceURL, and where new Function is not allowed (a Content Security Policy, Cloudflare Workers, node --disallow-code-generation-from-strings) the closure backend stays. The probe canEval() runs new Function('') once; under a CSP with a report-uri that one violation is reported, and under a report-only policy the probe succeeds and every build is reported. A check the generator cannot build is reported (compile().error, register({ onFallback })), never installed. The core entry never calls new Function. The checks are compiled on their first use (3.4), so a rule changed on a built field after construction reaches the generator too; it reaches the source as a literal or a name of the scope all the same.
  • Input shape is checked strictly. Every key that is not in the schema is reported as unexpected (has unexpected keys: ..., the first ten of them), including constructor, __proto__ and toString, unless the schema or the call says unknown: 'ignore'. Fields live in a null-prototype dictionary, so nothing on Object.prototype is ever read as a field, and metaschema never writes to the value it checks. A built-in (Map, Set, Date) is recognised by the name of its constructor and by its shape (an iterator, getTime), so a value parsed from JSON that carries { "constructor": { "name": "Map" } } is a type issue, not a value the check iterates. A value with a then function is data: the passes of parse and parseAsync await only the promises their own decoders make, never a thenable of the value. A graph nested deeper than maxDepth embedded records (128 by default) is a depth issue, not a stack overflow.
  • A pattern or a format is never run over a string longer than length.max. The bound is what keeps a backtracking expression fast, so give every pattern one (the lint says where one is missing); the built-in formats are linear and anchored, a custom format's test is the program's code, like a validate function. A pattern taken from a document given to fromJSONSchema or a snapshot given to fromJSON runs as a regular expression in check all the same: those documents are untrusted input for their structure (a keyword outside the subset, a key named __proto__, a depth past the limit are errors, never a looser schema), but a pattern in them is the importer's to review or to bound.
  • A snapshot is data, both ways. What schema.toSnapshot() and model.toJSON() return is a copy (the custom metadata of a kind, the indexes and the database metadata included): no function, no object of the model, a function as { $function: true }. Read back by fromJSON, a snapshot is untrusted input: its tags ($date, $bigint, $bytes, $map, $set) are read into values and a tag that does not read is ERR_INVALID_SNAPSHOT; a value shaped like a Date or a RegExp ({ "constructor": { "name": "Date" } }) is data; its root names one of its entities; a field key root is refused like the methods of a field; the walks are bounded (128 levels, and a chain of entities of any length orders without recursion). A model restored from a snapshot validates its structure with placeholders for the functions, which model.placeholders lists.
  • Binary data is bounded by its length. The bytes converter of ./coerce decodes canonical base64 only, in linear time, and leaves a text whose bytes would be over length.max undecoded, so a field with a length caps what a request can make it allocate; a bytes field without one decodes a text of any size, like a string field without length.max holds one. fromDocument of ./mongodb reads a generic Binary (subtype 0 or 2) into a copy of its bytes and leaves any other subtype to the check.
  • The definition entry does not validate. A schema built through @alexify/metaschema/definition throws ERR_NOT_REGISTERED from every way to validate until the core entry of the same copy of the package is loaded, and has no ~standard, so a consumer of Standard Schema never takes it for a validator. A schema of another copy (two versions of the package installed) does not validate with this copy's core and says so.
  • Samples are input. infer of the model subpath walks the values it is given with a depth limit, trusts no key (__proto__, constructor and the names of the methods of a field are keys of the definition, written as own properties), and checks the definition it built against every sample before it returns it. A key of a sample that reaches an error message is truncated to 100 characters, as a key of a value under check is.
  • The error a stream ends with carries the record. ERR_INVALID_RECORD of the stream subpath has record and result on it, since the stream carries them no further; a logger that serializes error objects writes the record.
  • Validation errors are data. check collects problems as issues (a code, a path and params, with a rendered message) instead of throwing on bad input. The messages contain field paths (which can include key names taken from the input, truncated to 100 characters) and, for enum, the allowed values; they never contain the rejected values themselves. A message from a custom validate function, or from an exception it throws, is passed through as written.
  • Type registration is process-wide. Custom types registered by one Model are visible to every schema in the process. Register them once, from trusted code.
  • saveTypes writes where it is told. The output path is the caller's argument; metaschema does not sanitize it.
  • Bundled and minified builds validate exactly like the source. Internal identity checks use a global symbol brand, not class names, and tests/unit/bundle.test.js validates through esbuild bundles of both entry points, minified and not. A build in which check accepts invalid data is a bug; please report it.

Things that are in scope: input data that makes check throw, hang or consume unbounded resources (a circular value is reported as is a circular reference, not recursed into); input data that changes a schema or the type registry; and a message that leaks a value it should not.

There aren't any published security advisories