Skip to content

Commit

Permalink
Create API-level tests for non-own property access
Browse files Browse the repository at this point in the history
  • Loading branch information
ericcornelissen committed Nov 3, 2023
1 parent 3182149 commit 600d719
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 0 deletions.
98 changes: 98 additions & 0 deletions test/integration/constructor/_pollution.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* @overview Contains helpers to detect non-own property access.
* @license MIT
*/

import assert from "node:assert/strict";

/**
* A store for non-own property accesses by proxy.
*/
const pollutionState = new Map();

/**
* Check whether or not a value is nil (`null` or `undefined`).
*
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is nil, `false` otherwise.
*/
function isNil(value) {
return value === null || value === undefined;
}

/**
* Check whether or not a value is a primitive value.
*
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is primitive, `false` otherwise.
*/
function isPrimitive(value) {
return typeof value === "number" || typeof value === "string";
}

/**
* Check whether or not a value can be wrapped by a `Proxy`.
*
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is proxyable, `false` otherwise.
*/
function isProxyable(value) {
return !(isNil(value) || isPrimitive(value));
}

/**
* Wrap the provided target in in order to monitor it for access to properties
* not present on the target.
*
* @param {any} target The value to wrap.
* @returns {any} The wrapped value (or original if wrapping isn't possible).
*/
export function wrap(target) {
if (!isProxyable(target)) {
return target;
}

const nonOwnAccesses = new Set();
const proxy = new Proxy(target, {
get(target, property, _proxy) {
if (!Object.hasOwn(target, property)) {
nonOwnAccesses.add(property);
}

// Return normal lookup to ensure normal test execution.
return target[property];
},
});

pollutionState.set(proxy, nonOwnAccesses);
return proxy;
}

/**
* Check if non-own property access was detected on the given wrapped object.
*
* @param {any} wrapped A `wrap`ped value.
* @throws {Error} If non-own property access was detected.
*/
export function check(wrapped) {
if (!isProxyable(wrapped)) {
return;
}

assert.ok(pollutionState.has(wrapped), "target not found");
const nonOwnAccesses = pollutionState.get(wrapped);

// Remove the proxy from the state so re-use of it does not result in errors
// from one test to affect other tests. (Also just to reduce memory usage.)
pollutionState.delete(wrapped);

const actual = nonOwnAccesses.size;
const expected = 0;
const propertiesList = Array.from(nonOwnAccesses.values()).join(", ");

assert.equal(
actual,
expected,
`Non0own access to ${actual} property(s) detected: ${propertiesList}`,
);
}
17 changes: 17 additions & 0 deletions test/integration/constructor/constructor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,29 @@
* @license MIT
*/

import { testProp } from "@fast-check/ava";
import test from "ava";

import * as pollution from "./_pollution.js";
import { arbitrary } from "../_.js";

import { Shescape } from "shescape";

test("shell is unsupported", (t) => {
const shell = "not-actually-a-shell-that-exists";

t.throws(() => new Shescape({ shell }), { instanceOf: Error });
});

testProp(
"affected by prototype pollution",
[arbitrary.shescapeOptions().map(pollution.wrap)],
(t, options) => {
try {
new Shescape(options);
} catch (_) {}

pollution.check(options);
t.pass();
},
);

0 comments on commit 600d719

Please sign in to comment.