@fast-check/poisoning
Version:
Set of utilities to ease detection and revert of poisoning
311 lines (310 loc) • 12.6 kB
JavaScript
//#region src/internals/PoisoningFreeArray.ts
const safeArrayFrom = Array.from;
const safeArrayMap = Array.prototype.map;
const safeArrayPush = Array.prototype.push;
const safeArrayShift = Array.prototype.shift;
const safeArraySort = Array.prototype.sort;
const safeObjectDefineProperty$3 = Object.defineProperty;
/** Alias for Array.prototype.map */
const MapSymbol = Symbol("safe.map");
/** Alias for Array.prototype.push */
const PushSymbol = Symbol("safe.push");
/** Alias for Array.prototype.shift */
const ShiftSymbol = Symbol("safe.shift");
/** Alias for Array.prototype.sort */
const SortSymbol = Symbol("safe.sort");
/** Alter an instance of Array to include non-poisonable methods */
function toPoisoningFreeArray(instance) {
safeObjectDefineProperty$3(instance, MapSymbol, {
value: safeArrayMap,
configurable: false,
enumerable: false,
writable: false
});
safeObjectDefineProperty$3(instance, PushSymbol, {
value: safeArrayPush,
configurable: false,
enumerable: false,
writable: false
});
safeObjectDefineProperty$3(instance, ShiftSymbol, {
value: safeArrayShift,
configurable: false,
enumerable: false,
writable: false
});
safeObjectDefineProperty$3(instance, SortSymbol, {
value: safeArraySort,
configurable: false,
enumerable: false,
writable: false
});
return instance;
}
/** Factory responsible to build instances of PoisoningFreeArray */
const PoisoningFreeArray = { from(arrayLike) {
return toPoisoningFreeArray(safeArrayFrom(arrayLike));
} };
//#endregion
//#region src/internals/PoisoningFreeMap.ts
const SMap = Map;
const safeMapGet = Map.prototype.get;
const safeMapHas = Map.prototype.has;
const safeMapEntries = Map.prototype.entries;
const safeMapSet = Map.prototype.set;
const safeObjectDefineProperty$2 = Object.defineProperty;
/** Alias for Map.prototype.get */
const GetSymbol = Symbol("safe.get");
/** Alias for Map.prototype.has */
const HasSymbol$1 = Symbol("safe.has");
/** Alias for Map.prototype.entries */
const EntriesSymbol = Symbol("safe.entries");
/** Alias for Map.prototype.set */
const SetSymbol = Symbol("safe.set");
/** Alter an instance of Map to include non-poisonable methods */
function toPoisoningFreeMap(instance) {
safeObjectDefineProperty$2(instance, GetSymbol, {
value: safeMapGet,
configurable: false,
enumerable: false,
writable: false
});
safeObjectDefineProperty$2(instance, HasSymbol$1, {
value: safeMapHas,
configurable: false,
enumerable: false,
writable: false
});
safeObjectDefineProperty$2(instance, EntriesSymbol, {
value: safeMapEntries,
configurable: false,
enumerable: false,
writable: false
});
safeObjectDefineProperty$2(instance, SetSymbol, {
value: safeMapSet,
configurable: false,
enumerable: false,
writable: false
});
return instance;
}
/** Factory responsible to build instances of PoisoningFreeMap */
const PoisoningFreeMap = { from(ins) {
return toPoisoningFreeMap(new SMap(ins));
} };
//#endregion
//#region src/internals/PoisoningFreeSet.ts
const SSet = Set;
const safeSetAdd = Set.prototype.add;
const safeSetHas = Set.prototype.has;
const safeObjectDefineProperty$1 = Object.defineProperty;
/** Alias for Set.prototype.add */
const AddSymbol = Symbol("safe.add");
/** Alias for Set.prototype.has */
const HasSymbol = Symbol("safe.has");
/** Alter an instance of Set to include non-poisonable methods */
function toPoisoningFreeSet(instance) {
safeObjectDefineProperty$1(instance, AddSymbol, {
value: safeSetAdd,
configurable: false,
enumerable: false,
writable: false
});
safeObjectDefineProperty$1(instance, HasSymbol, {
value: safeSetHas,
configurable: false,
enumerable: false,
writable: false
});
return instance;
}
/** Factory responsible to build instances of PoisoningFreeMap */
const PoisoningFreeSet = { from(ins) {
return toPoisoningFreeSet(new SSet(ins));
} };
//#endregion
//#region src/internals/CaptureAllGlobals.ts
const SString$1 = String;
const safeObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors;
const safeObjectGetOwnPropertyNames$1 = Object.getOwnPropertyNames;
const safeObjectGetOwnPropertySymbols$1 = Object.getOwnPropertySymbols;
function compareKeys(keyA, keyB) {
const sA = SString$1(keyA[0]);
const sB = SString$1(keyB[0]);
return sA < sB ? -1 : sA > sB ? 1 : 0;
}
function extractAllDescriptorsDetails(instance) {
const descriptors = safeObjectGetOwnPropertyDescriptors(instance);
const allDescriptors = PoisoningFreeArray.from([...safeObjectGetOwnPropertyNames$1(descriptors), ...safeObjectGetOwnPropertySymbols$1(descriptors)]);
return PoisoningFreeArray.from(allDescriptors[MapSymbol]((name) => [name, descriptors[name]]))[SortSymbol](compareKeys);
}
/** Flag the roots on already existing globals */
function flagRootRecursively(knownGlobals, instance, currentRoot) {
const storedGlobal = knownGlobals[GetSymbol](instance);
if (storedGlobal === void 0) return;
if (storedGlobal.rootAncestors[HasSymbol](currentRoot)) return;
storedGlobal.rootAncestors[AddSymbol](currentRoot);
if (storedGlobal.depth <= 1) return;
for (const [, descriptor] of storedGlobal.properties) flagRootRecursively(knownGlobals, descriptor.value, currentRoot);
}
/** Capture all globals accessible from globalThis */
function captureAllGlobals() {
const knownGlobals = PoisoningFreeMap.from();
const nextCaptures = PoisoningFreeArray.from([{
instance: globalThis,
name: "globalThis",
currentDepth: 0,
lastRootInPath: "globalThis"
}]);
while (nextCaptures.length !== 0) {
const { instance, name, currentDepth, lastRootInPath } = nextCaptures[ShiftSymbol]();
if (typeof instance !== "function" && typeof instance !== "object") continue;
if (instance === null || instance === void 0) continue;
if (knownGlobals[HasSymbol$1](instance)) {
flagRootRecursively(knownGlobals, instance, lastRootInPath);
continue;
}
const allDescriptorsDetails = extractAllDescriptorsDetails(instance);
const localGlobal = {
name,
depth: currentDepth,
properties: PoisoningFreeMap.from(),
rootAncestors: PoisoningFreeSet.from([lastRootInPath])
};
knownGlobals[SetSymbol](instance, localGlobal);
for (let index = 0; index !== allDescriptorsDetails.length; ++index) {
const descriptorName = allDescriptorsDetails[index][0];
const descriptor = allDescriptorsDetails[index][1];
localGlobal.properties[SetSymbol](descriptorName, descriptor);
if (typeof descriptorName === "symbol") continue;
const subGlobalName = currentDepth !== 0 ? name + "." + SString$1(descriptorName) : SString$1(descriptorName);
const newLastRootInPath = currentDepth <= 1 ? name : lastRootInPath;
nextCaptures[PushSymbol]({
instance: descriptor.value,
name: subGlobalName,
currentDepth: currentDepth + 1,
lastRootInPath: newLastRootInPath
});
}
}
return knownGlobals;
}
//#endregion
//#region src/internals/FilterNonEligibleDiffs.ts
/** Check whether or not a global has to be ignored for diff tracking */
function shouldIgnoreGlobal(globalDetails, ignoredRootRegex) {
switch (globalDetails.depth) {
case 0: return false;
case 1: return ignoredRootRegex.test(globalDetails.name);
default: {
let allRootsIgnored = true;
const allRoots = [...globalDetails.rootAncestors];
for (let rootIndex = 0; rootIndex !== allRoots.length; ++rootIndex) allRootsIgnored = allRootsIgnored && ignoredRootRegex.test(allRoots[rootIndex]);
return allRootsIgnored;
}
}
}
/** Check whether or not a property from a global has to be ignored for diff tracking */
function shouldIgnoreProperty(globalDetails, propertyName, ignoredRootRegex) {
return globalDetails.depth === 0 && ignoredRootRegex.test(propertyName);
}
//#endregion
//#region src/internals/TrackDiffsOnGlobal.ts
const SString = String;
const safeObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const safeObjectGetOwnPropertyNames = Object.getOwnPropertyNames;
const safeObjectGetOwnPropertySymbols = Object.getOwnPropertySymbols;
const safeObjectIs = Object.is;
const safeObjectDefineProperty = Object.defineProperty;
/** Compute the diff between two versions of globals */
function trackDiffsOnGlobals(initialGlobals, isEligibleGlobal, isEligibleProperty) {
const allInitialGlobals = [...initialGlobals[EntriesSymbol]()];
const observedDiffs = PoisoningFreeArray.from([]);
for (let index = 0; index !== allInitialGlobals.length; ++index) {
const instance = allInitialGlobals[index][0];
const globalDetails = allInitialGlobals[index][1];
if (!isEligibleGlobal(globalDetails)) continue;
const name = globalDetails.name;
const initialProperties = globalDetails.properties;
const initialPropertiesList = [...initialProperties[EntriesSymbol]()];
for (let propertyIndex = 0; propertyIndex !== initialPropertiesList.length; ++propertyIndex) {
const propertyName = initialPropertiesList[propertyIndex][0];
const initialPropertyDescriptor = initialPropertiesList[propertyIndex][1];
if (!isEligibleProperty(globalDetails, SString(propertyName))) continue;
const currentDescriptor = safeObjectGetOwnPropertyDescriptor(instance, propertyName);
if (currentDescriptor === void 0) observedDiffs[PushSymbol]({
keyName: SString(propertyName),
fullyQualifiedKeyName: name + "." + SString(propertyName),
type: "removed",
patch: () => {
safeObjectDefineProperty(instance, propertyName, initialPropertyDescriptor);
},
globalDetails
});
else if (!safeObjectIs(initialPropertyDescriptor.value, currentDescriptor.value) || !safeObjectIs(initialPropertyDescriptor.get, currentDescriptor.get) || !safeObjectIs(initialPropertyDescriptor.set, currentDescriptor.set)) observedDiffs[PushSymbol]({
keyName: SString(propertyName),
fullyQualifiedKeyName: name + "." + SString(propertyName),
type: "changed",
patch: () => {
safeObjectDefineProperty(instance, propertyName, initialPropertyDescriptor);
},
globalDetails
});
}
const currentDescriptorsList = [...safeObjectGetOwnPropertyNames(instance), ...safeObjectGetOwnPropertySymbols(instance)];
for (let descriptorIndex = 0; descriptorIndex !== currentDescriptorsList.length; ++descriptorIndex) {
const propertyName = currentDescriptorsList[descriptorIndex];
if (!isEligibleProperty(globalDetails, SString(propertyName))) continue;
if (!initialProperties[HasSymbol$1](propertyName)) observedDiffs[PushSymbol]({
keyName: SString(propertyName),
fullyQualifiedKeyName: name + "." + SString(propertyName),
type: "added",
patch: () => {
delete instance[propertyName];
},
globalDetails
});
}
}
return [...observedDiffs];
}
//#endregion
//#region src/main.ts
const initialGlobals = captureAllGlobals();
/** Internal helper to share the extraction logic */
function trackDiffsOnGlobalsBasedOnOptions(options) {
const ignoredRootRegex = options !== void 0 && options.ignoredRootRegex !== void 0 ? options.ignoredRootRegex : void 0;
return ignoredRootRegex !== void 0 ? trackDiffsOnGlobals(initialGlobals, (globalDetails) => !shouldIgnoreGlobal(globalDetails, ignoredRootRegex), (globalDetails, propertyName) => !shouldIgnoreProperty(globalDetails, propertyName, ignoredRootRegex)) : trackDiffsOnGlobals(initialGlobals, () => true, () => true);
}
/**
* Restore all globals as they were when first importing this package.
*
* Remark: At least, it attempts to do so
*/
function restoreGlobals(options) {
const diffs = trackDiffsOnGlobalsBasedOnOptions(options);
for (let index = 0; index !== diffs.length; ++index) diffs[index].patch();
}
/**
* Check whether or not some globlas have been poisoned by some code.
*
* Poisoned being one of the following changes:
* - a new entity is accessible directly or indirectly from `globalThis`
* - an entity referenced directly or indirectly on `globalThis` has been altered
* - an entity referenced directly or indirectly on `globalThis` has been dropped
*
* Here are some examples of such changes:
* - someone added a new global on `window` (browser case) or `global` (node case) or modern `globalThis` (everywhere)
* - someone changed `Array.prototype.map` into another function
*/
function assertNoPoisoning(options) {
const diffs = trackDiffsOnGlobalsBasedOnOptions(options);
if (diffs.length !== 0) {
let impactedElements = diffs[0].fullyQualifiedKeyName;
for (let index = 1; index !== diffs.length; ++index) impactedElements += ", " + diffs[index].fullyQualifiedKeyName;
throw new Error("Poisoning detected on " + impactedElements);
}
}
//#endregion
export { assertNoPoisoning, restoreGlobals };