Your validator checked one value. Your app used another.
3 min read

Updated

Your validator checked one value. Your app used another.

JavaScript properties can change between reads. Validation needs to account for the value the application actually uses, especially when inputs contain getters or proxies.

A JavaScript property doesn’t have to return the same value twice. If a validator reads it once and the application reads it again, the two can see different data.

That becomes a security problem when the first read passes an allowlist and the second controls an action.

The key question is simple: does the application use the value that was checked?

A small example

This example demonstrates the language behavior without depending on a particular validation library:

let reads = 0;
const input = {
  get role() {
    return ++reads === 1 ? "readonly" : "admin";
  },
};

const allowed = new Set(["readonly", "guest"]);
const passes = allowed.has(input.role);

console.log(passes);    // true
console.log(input.role); // "admin"

The allowlist is correct. The problem is that the property was read twice.

An accessor can execute code on a read. A proxy can intercept a read. A getter can also derive its answer from state that changes between validation and use. Returning the original object leaves those behaviors attached to it.

Two branches representing validation and later use of a value

Check what the API returns

Some validation APIs inspect an object and report errors. Others return parsed data for the caller to use. Those contracts are different.

When an API validates the original object in place, callers need to understand whether its properties remain stable. When an API returns a new value, callers must actually use that value instead of continuing to read the original input.

A snapshot can close the gap if the application validates and uses the same captured data. For the scalar example:

const checkedRole = input.role;
if (!allowed.has(checkedRole)) {
  throw new Error("Role is not allowed");
}
// Use checkedRole from here onward.

For nested structures, a shallow copy isn’t enough. Nested objects can still contain accessors or mutable state. The parser needs an explicit policy for the values and types it accepts, and for how it materializes them.

Where the issue is reachable

An ordinary JSON request body doesn’t carry executable getters. Parsing JSON produces data properties. Turning this observation into an application vulnerability therefore requires another path: an accessor-bearing class instance, a proxy, or an object derived from mutable or attacker-influenced state.

That condition belongs in the report. A JavaScript demonstration alone doesn’t establish remote exploitability in every application using a validation package.

It also doesn’t establish that one library is universally safe and another universally unsafe. Versions, schema options, transformations, and the caller’s use of the result all matter.

What we reported, and a correction

We raised public hardening reports for class-validator and superstruct. They describe the gap between checking a live object and using its properties later. A public report is not the same as maintainer confirmation or a released fix.

The earlier version of this article included a class-validator example that copied a getter with Object.assign. That was incorrect: assignment invokes the source getter and copies its returned value; it doesn’t preserve the accessor. The example above isolates the actual read-instability behavior instead of presenting that snippet as a working library reproducer.

When testing this class of issue, record each property access, the value observed during validation, and the value used at the sensitive operation. The evidence needs all three.