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) |
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.
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
validatefunctions, custom types'construct/checkType, calculated fields, theparseandserializefunctions of a field and of a schema (decoders and serializers), anddefaultfactories 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 leavescheck. Build schemas only from definitions you trust, the same as any other module yourequire. - 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/compilesubpath (3.0), and only when the application calls itscompile()orregister(): 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 asourceURL, and wherenew Functionis not allowed (a Content Security Policy, Cloudflare Workers,node --disallow-code-generation-from-strings) the closure backend stays. The probecanEval()runsnew Function('')once; under a CSP with areport-urithat 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 callsnew 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), includingconstructor,__proto__andtoString, unless the schema or the call saysunknown: 'ignore'. Fields live in a null-prototype dictionary, so nothing onObject.prototypeis 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 athenfunction is data: the passes ofparseandparseAsyncawait only the promises their own decoders make, never a thenable of the value. A graph nested deeper thanmaxDepthembedded records (128 by default) is adepthissue, not a stack overflow. - A
patternor aformatis never run over a string longer thanlength.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'stestis the program's code, like avalidatefunction. A pattern taken from a document given tofromJSONSchemaor a snapshot given tofromJSONruns as a regular expression incheckall 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()andmodel.toJSON()return is a copy (thecustommetadata of a kind, the indexes and thedatabasemetadata included): no function, no object of the model, a function as{ $function: true }. Read back byfromJSON, a snapshot is untrusted input: its tags ($date,$bigint,$bytes,$map,$set) are read into values and a tag that does not read isERR_INVALID_SNAPSHOT; a value shaped like aDateor aRegExp({ "constructor": { "name": "Date" } }) is data; itsrootnames one of its entities; a field keyrootis 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, whichmodel.placeholderslists. - Binary data is bounded by its
length. Thebytesconverter of./coercedecodes canonical base64 only, in linear time, and leaves a text whose bytes would be overlength.maxundecoded, so a field with alengthcaps what a request can make it allocate; abytesfield without one decodes a text of any size, like a string field withoutlength.maxholds one.fromDocumentof./mongodbreads 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/definitionthrowsERR_NOT_REGISTEREDfrom 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.
inferof the model subpath walks the values it is given with a depth limit, trusts no key (__proto__,constructorand 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_RECORDof the stream subpath hasrecordandresulton it, since the stream carries them no further; a logger that serializes error objects writes the record. - Validation errors are data.
checkcollects 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, forenum, the allowed values; they never contain the rejected values themselves. A message from a customvalidatefunction, or from an exception it throws, is passed through as written. - Type registration is process-wide. Custom types registered by one
Modelare visible to every schema in the process. Register them once, from trusted code. saveTypeswrites 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.jsvalidates through esbuild bundles of both entry points, minified and not. A build in whichcheckaccepts 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.