A test kit for detecting prototype pollution and gadgets
A testing utility library to help write tests related to [prototype pollution]
such as behavior under pollution and missing property accesses.
Introduction |
Documentation |
Changelog |
Contributing Guidelines |
Security Policy |
LICENSE
[Prototype pollution] allows attackers to manipulate the properties available on
the prototype of an object. Such properties might be accessed in code when you
look up a property that's not defined directly on the object.
For example:
``javascript
// What pollution looks like in simplified terms:
Object.prototype.foo = "bar";
// The result prototype pollution might have on your code:
const obj = { hello: "world!" };
if (obj.foo === "bar") {
// This will get executed because foo is set to bar on the prototype
console.log("The object has the property 'foo' with the value 'bar'");
}
`
To mitigate this you can (among other strategies) check if the property is
present on the object itself before accessing it. This would look like:
`javascript`
const obj = { hello: "world!" };
if (Object.hasOwn(obj, "foo") && obj.foo === "bar") {
// This won't get executed because obj does not have an own property named foo
console.log("The object has the property 'foo' with the value 'bar'");
}
However, it's pretty easy to forget to write this check...
To help protect against the effects of prototype pollution this project offers
testing utilities that you can use in your tests to simulate pollution or detect
missing property accesses.
The philosophy behind doing this is that any such missing lookup could result
in unexpected behavior due to prototype pollution. Because unexpected behavior
is unwanted, you want them either to not occur or ensure that the behavior is
still as expected.
Additionally, tests can give you confidence to remove unnecessary or duplicate
checks that protect against prototype pollution - which improves performance.
There are two categories of test utilities in this library. Each help you write
different types of tests. It is recommended to read the introduction for each
category to understand which you should use.
Missing Property Access |
Simulated Pollution
A missing property access occurs when your code tried to access a property that
the object does not have, for example: {}.foo. If this happens, the behaviorfoo
of that code is affected by prototype pollution. If occurs is present in"bar"
the prototype as the example will return "bar" instead of undefined.
Because this can have negative consequences this library includes utilities to
test for missing property accesses.
#### throwing.wrap(subject[, options])
- subject (function | object): The function or object to wrap.options
- (object):[strict]
- (boolean, default false): Set to true to disallow lookups of
properties currently in the prototype. This allows for use of the prototype
chain for inheritance purposes. Since this is generally okay and expected
this is not enabled by default, though it should be noted that such
properties can be overridden which may have unexpected consequences.
- Returns: A wrapped object.
Wrap a function or object so that the first missing property access results in
an error being thrown. For functions it covers all its arguments. The error will
include the name of the property being accessed as well as the code location
where the access occurred.
This more or less requires that you don't expect the function to error in the
first place. It may also succeed unexpectedly if the missing property access
happens inside a try-catch statement. Alternatively, consider using the manual
API instead.
For example for functions:
`javascript
import { test } from "node:test";
import * as ppTestKit from "pp-test-kit/throwing";
import { fn } from "./my-file.js";
test("no missing properties are accessed", () => {
// Wrap the function under test using the test kit so that any missing
// property accesses on any of its arguments is detected.
const fut = ppTestKit.wrap(fn);
// Run the wrapped function under test. This will throw an error if a missing
// property is accessed.
fut({});
});
`
and for objects:
`javascript
import { test } from "node:test";
import * as ppTestKit from "pp-test-kit/throwing";
import { fn } from "./my-file.js";
test("no missing properties are accessed", () => {
// Wrap at least one input to the function under test. The first missing
// property access on any wrapped object will result in an error.
const options = ppTestKit.wrap({});
// Run the function under test. This will throw an error if a missing
// property is accessed.
fn(options);
});
`
This will error on the first missing property access detected when running fn.
Subsequent missing property accesses won't be detected.
A more comprehensive approach is to use [property testing] to generate varied
input arguments to the function under test. See the [examples] to get an idea
of how this can be done.
#### manual.wrap(subject[, options])
- subject (function | object): The function or object to wrap.options
- (object):[strict]
- (boolean, default false): Set to true to disallow lookups of
properties currently in the prototype. This allows for use of the prototype
chain for inheritance purposes. Since this is generally okay and expected
this is not enabled by default, though it should be noted that such
properties can be overridden which may have unexpected consequences.
- Returns: A wrapped object.
Wrap a function or object so that all missing property accesses are recorded and
can later be checked with the manual.check(...wrapped) API. For functions it
covers all its arguments. If any missing property accesses occurred the check
will error with the name and corresponding code location of the access for each
detected access.
This requires that you manually check the wrapped object(s) after running the
function under test. If you don't do this nothing will happen. Alternatively,
consider using the throwing API instead.
For example for functions:
`javascript
import { test } from "node:test";
import * as ppTestKit from "pp-test-kit/manual";
import { fn } from "./my-file.js";
test("no missing properties are accessed", () => {
// Wrap the function under test using the test kit so that any missing
// property accesses on any of its arguments is detected.
const fut = ppTestKit.wrap(fn);
// Run the wrapped function under test. This should succeed.
fut({});
// Check the wrapped object(s) for any missing property accesses. If any
// missing property access occurred this will throw an error.
ppTestKit.check(fut);
});
`
and for objects:
`javascript
import { test } from "node:test";
import * as ppTestKit from "pp-test-kit/manual";
import { fn } from "./my-file.js";
test("no missing properties are accessed", () => {
// Wrap at least one input to the function under test. Each missing property
// access on any wrapped object will be recorded and must be checked later.
const options = ppTestKit.wrap({});
// Run the function under test. This should succeed.
fn(options);
// Check the wrapped object(s) for any missing property accesses. If any
// missing property access occurred this will throw an error.
ppTestKit.check(options);
});
`
This will error on all missing property accesses detected when running fn.
A more comprehensive approach is to use [property testing] to generate varied
input arguments to the function under test. See the [examples] to get an idea
of how this can be done.
Looking up values in the prototype might not always be bad. To be sure that this
is the case these testing utilities help you simulate pollution so that you can
test that this is indeed the case.
#### invariant.test(fn)
- fn (function) A test function. Accepts one argument, the test context ctx,
which provides further utilities.
Run a test function exposed to pollution of detected missing property accesses.
Useful when avoiding missing property accesses isn't an option.
The test function will always be called at least once. This initial run occurs
without any pollution to see if the test succeeds and detect an missing property
accesses. Subsequent runs simulate pollution of detected missing property. There
will also be a run simulating pollution of an enumerable property which may
affect iterations.
The be able to detect missing property accesses you must wrap at least one input
to the function under test.
For example:
`javascript
import { test } from "node:test";
import * as ppTestKit from "pp-test-kit/invariant";
import { fn } from "./my-file.js";
test("polluted properties don't affect the function", () => {
// Run the test kit's test function inside your test frameworks test function.
ppTestKit.test((ctx) => {
// Wrap at least one input to the function under test. This serves both to
// monitor for missing property accesses as well as simulating pollution.
const options = ctx.wrap({});
// Run the function under test.
const result = fn(options);
// Assert some invariant that should hold even when the function under test
// is exposed to prototype pollution.
assert.ok(result);
});
});
`
This will error if the invariant (assert.ok in this case) does not hold. If
this happens in the initial unpolluted run it will indicate so in the error
message. If this happens when exposed to pollution the error will include the
property and code location of first access.
A more comprehensive approach is to use [property testing] to generate varied
input arguments to the function under test. However, this may be slow if many
missing property accesses occur.
##### ctx.wrap(subject[, options])
- subject (object): The object to wrap.options
- (object):[strict]
- (boolean, default false): Set to true to disallow lookups of
properties currently in the prototype. This allows for use of the prototype
chain for inheritance purposes. Since this is generally okay and expected
this is not enabled by default, though it should be noted that such
properties can be overridden which may have unexpected consequences.
- Returns: A wrapped object.
#### simulate.simulatePollution(subject, ...optionsList)
- subject (object): The object to simulate pollution on....optionsList
- (object):[enumerable]
- (boolean, default: false): Whether or not the propertyproperty
should be enumerable.
- (any): The property to pollute.[value]
- (any, default: _random string_): The value to pollute with.
- Returns: An object affected by pollution.
Simulate the effect of prototype pollution of specific properties on a single
object. Useful for regression testing or when a property is known to be security
sensitive.
This affects only one object, subject, and no other objects. Alternatively,withPollution
consider using the API
to have pollution affect all objects.
For example:
`javascript
import { test } from "node:test";
import * as ppTestKit from "pp-test-kit/simulate";
import { fn } from "./my-file.js";
test("polluting 'foo' doesn't affect the function", () => {
const options = {};
// Simulate pollution on at least one input to the function under test.
const pollutedOptions = ppTestKit.simulatePollution(options, {
property: "foo",
value: "bar",
});
// Run the function under test.
const actual = fn(pollutedOptions);
const expected = fn(options);
// Assert that pollution does not affect the result.
assert.equal(actual, expected);
});
`
This asserts that the function under tests has the same result regardless of
whether the foo property is polluted.
A more comprehensive approach is to use [property testing] to generate varied
input arguments to the function under test. See the [examples] to get an idea
of how this can be done.
#### simulate.withPollution(fn, ...optionsList)
- fn (Function): The function to run with pollution simulated....optionsList
- (object):[enumerable]
- (boolean, default: false): Whether or not the propertyproperty
should be enumerable.
- (any): The property to pollute.[value]
- (any, default: _random string_): The value to pollute with.fn
- Returns: The return value of .
Simulate the effect of prototype pollution of specific properties in general.
Useful for regression testing or when a property is known to be security
sensitive.
This affects the entire program for the duration of fn, including your testsimulatePollution
framework and anything else that might execute. Alternatively, consider using
the API to
have pollution affect only a single object.
For example:
`javascript
import { test } from "node:test";
import * as ppTestKit from "pp-test-kit/simulate";
import { fn } from "./my-file.js";
test("polluting 'foo' doesn't affect the function", () => {
const options = {};
// Run the function under test under the effect of prototype pollution.
ppTestKit.withPollution(() => fn(options), {
property: "foo",
value: "bar",
});
// Assert that pollution does not affect the result.
const expected = fn(options);
assert.equal(actual, expected);
});
`
This asserts that the function under tests has the same result regardless of
whether the foo` property is polluted.
A more comprehensive approach is to use [property testing] to generate varied
input arguments to the function under test. See the [examples] to get an idea
of how this can be done.
Property testing (or generative testing or QuickCheck testing) is a automated
testing technique where the function under test is exposed to a wide variety of
inputs and the same property is asserted for each. In the context of this
library the property would relate to the effect of prototype pollution on the
function under test.
In JavaScript you can write property tests using [fast-check].
Prototype pollution is a vulnerability category affecting JavaScript that allows
attackers to modify prototype objects. Since inheritance in JavaScript relies on
prototypes, this means that when a missing property is accessed on an object it
might return the polluted value. Depending on how the value is used, the
consequences can range from minor issues to severe security risks.
You can learn more at
[examples]: ./examples/
[fast-check]: https://www.npmjs.com/package/fast-check
[property testing]: #property-testing
[prototype pollution]: #prototype-pollution