Last active
April 2, 2025 17:37
-
-
Save dougwithseismic/156ae2d424e687d257630cd92aaf00d6 to your computer and use it in GitHub Desktop.
Start Reversing Literally ANY React App
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Configuration object for the search | |
const searchConfig = { | |
searchCriteria: { user: { id: true, email: true } }, // What we're looking for | |
maxDepth: 50, // How deep to search in the tree | |
stopAfterFirst: false, // Whether to stop after finding the first match | |
searchPaths: ["memoizedProps", "memoizedState"], // Where to look in each node | |
mainSelector: "#__next", // The root element of our React app | |
callback: (matchingObjects) => { | |
matchingObjects.forEach(({ matchingObject, fiberNode }) => { | |
console.log("Found matching object:", matchingObject); | |
console.log(fiberNode) | |
// Uncomment the next line to force a rerender on each match | |
// forceRerender(fiberNode); | |
}); | |
} | |
}; | |
// Main function to search the React Fiber tree | |
function searchReactFiber(config) { | |
const results = []; | |
const visitedNodes = new WeakSet(); | |
// Helper function to safely access object properties | |
function safelyAccessProperty(obj, prop) { | |
try { | |
return obj[prop]; | |
} catch (error) { | |
if (error instanceof DOMException && error.name === "SecurityError") { | |
return null; // Silently skip security-restricted properties | |
} | |
throw error; | |
} | |
} | |
// Check if an object matches our search criteria | |
function isMatchingObject(obj, criteria) { | |
if (typeof obj !== "object" || obj === null) return false; | |
for (const [key, value] of Object.entries(criteria)) { | |
const objValue = safelyAccessProperty(obj, key); | |
if (objValue === null) return false; | |
if (typeof value === "object" && value !== null) { | |
if (!isMatchingObject(objValue, value)) return false; | |
} else if (value === true) { | |
if (objValue === undefined) return false; | |
} else { | |
if (objValue !== value) return false; | |
} | |
} | |
return true; | |
} | |
// Traverse the Fiber tree | |
function traverseFiberTree(startNode) { | |
const stack = [{ node: startNode, depth: 0 }]; | |
while (stack.length > 0) { | |
const { node, depth } = stack.pop(); | |
if (!node || typeof node !== "object" || depth > config.maxDepth || visitedNodes.has(node)) { | |
continue; | |
} | |
visitedNodes.add(node); | |
// Check searchPaths for matching objects | |
for (const propName of config.searchPaths) { | |
const propValue = safelyAccessProperty(node, propName); | |
if (propValue && typeof propValue === "object") { | |
if (isMatchingObject(propValue, config.searchCriteria)) { | |
results.push({ matchingObject: propValue, fiberNode: node }); | |
if (config.stopAfterFirst) return; | |
} | |
// Search nested objects in memoizedProps and memoizedState | |
if (propName === "memoizedProps" || propName === "memoizedState") { | |
searchNestedObjects(propValue, node); | |
} | |
} | |
} | |
// Add child and sibling nodes to the stack | |
const child = safelyAccessProperty(node, 'child'); | |
if (child) stack.push({ node: child, depth: depth + 1 }); | |
const sibling = safelyAccessProperty(node, 'sibling'); | |
if (sibling) stack.push({ node: sibling, depth }); | |
} | |
} | |
// Search nested objects within a node | |
function searchNestedObjects(obj, fiberNode) { | |
const stack = [obj]; | |
const visited = new WeakSet(); | |
while (stack.length > 0) { | |
const current = stack.pop(); | |
if (typeof current !== "object" || current === null || visited.has(current)) { | |
continue; | |
} | |
visited.add(current); | |
if (isMatchingObject(current, config.searchCriteria)) { | |
results.push({ matchingObject: current, fiberNode }); | |
if (config.stopAfterFirst) return; | |
} | |
// Search keys that are likely to contain relevant data | |
const keysToSearch = Object.keys(current).filter(key => | |
typeof current[key] === "object" && | |
current[key] !== null && | |
!Array.isArray(current[key]) && | |
key !== "$$typeof" && | |
!key.startsWith("_") | |
); | |
for (const key of keysToSearch) { | |
const value = safelyAccessProperty(current, key); | |
if (value !== null) stack.push(value); | |
} | |
} | |
} | |
// Get the root fiber node | |
const main = document.querySelector(config.mainSelector); | |
if (!main) { | |
console.warn(`Main element not found with selector: ${config.mainSelector}`); | |
return results; | |
} | |
const fiberKey = Object.keys(main).find((key) => key.startsWith("__react")); | |
if (!fiberKey) { | |
console.warn("React fiber key not found. This may not be a React application or the fiber structure has changed."); | |
return results; | |
} | |
const fiberNode = safelyAccessProperty(main, fiberKey); | |
if (!fiberNode) { | |
console.warn("Unable to access fiber node. Skipping search."); | |
return results; | |
} | |
// Start the search | |
traverseFiberTree(fiberNode); | |
// Call the callback function if provided | |
if (typeof config.callback === 'function') { | |
config.callback(results); | |
} | |
return results; | |
} | |
// Helper function to force rerender a component | |
function forceRerender(fiber) { | |
while (fiber && !fiber.stateNode?.forceUpdate) { | |
fiber = fiber.return; | |
} | |
if (fiber && fiber.stateNode) { | |
fiber.stateNode.forceUpdate(); | |
} | |
} | |
// Execute the search | |
console.time("Search time"); | |
const matchingObjects = searchReactFiber(searchConfig); | |
console.timeEnd("Search time"); | |
console.log(`Found ${matchingObjects.length} matching objects`); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Get the root fiber node | |
const main = document.querySelector("#__next"); | |
const fiberKey = Object.keys(main).find((key) => key.startsWith("__react")); | |
const fiberNode = main[fiberKey]; | |
const visitedNodes = new WeakSet(); | |
const PATCH_SYMBOL = Symbol('WS_PATCHED'); | |
function isObjectOrArray(value) { | |
return (typeof value === "object" && value !== null) || Array.isArray(value); | |
} | |
function isStyleObject(obj) { | |
try { | |
return obj && typeof obj === "object" && Object.keys(obj).some((key) => | |
["color", "fontSize", "margin", "padding", "display", "position"].includes(key) | |
); | |
} catch (error) { | |
console.warn("Error in isStyleObject:", error); | |
return false; | |
} | |
} | |
function isPromise(p) { | |
return p && typeof p.then === 'function'; | |
} | |
function logPromiseFunctions(obj) { | |
if (!obj || typeof obj !== "object" || visitedNodes.has(obj)) return; | |
visitedNodes.add(obj); | |
Object.entries(obj).forEach(([key, value]) => { | |
try { | |
if (typeof value === "function" && !value[PATCH_SYMBOL]) { | |
const patchedFunction = new Proxy(value, { | |
apply: function(target, thisArg, argumentsList) { | |
console.log(`Promise function called with args:`, argumentsList); | |
const result = target.apply(thisArg, argumentsList); | |
if (isPromise(result)) { | |
result.then( | |
(res) => console.log(`Promise resolved:`, res), | |
(err) => console.log(`Promise rejected:`, err) | |
); | |
} | |
return result; | |
} | |
}); | |
patchedFunction[PATCH_SYMBOL] = true; | |
Object.defineProperty(patchedFunction, 'name', { value: `${value.name || 'anonymous'}_patched` }); | |
obj[key] = patchedFunction; | |
} else if (isObjectOrArray(value) && !isStyleObject(value)) { | |
logPromiseFunctions(value); | |
} | |
} catch (error) { | |
console.warn(`Error processing function:`, error); | |
} | |
}); | |
} | |
function traverseFiberTree(startNode) { | |
const stack = [startNode]; | |
while (stack.length > 0) { | |
try { | |
const node = stack.pop(); | |
if (!node || typeof node !== "object" || visitedNodes.has(node)) continue; | |
visitedNodes.add(node); | |
// Log functions in memoizedProps | |
if (node.memoizedProps && typeof node.memoizedProps === "object") { | |
logPromiseFunctions(node.memoizedProps); | |
} | |
// Add child and sibling to the stack | |
if (node.child) stack.push(node.child); | |
if (node.sibling) stack.push(node.sibling); | |
} catch (error) { | |
console.warn("Error in traverseFiberTree:", error); | |
} | |
} | |
} | |
// Start the traversal | |
console.log("Starting marked-patch Promise function logging in fiber tree:"); | |
try { | |
traverseFiberTree(fiberNode); | |
console.log("Finished setting up Promise function logging. Check console for function calls and Promise resolutions."); | |
} catch (error) { | |
console.error("Fatal error in fiber tree traversal:", error); | |
} | |
console.log("You can identify patched functions by checking for the presence of the WS_PATCHED symbol."); | |
console.log("Example usage: if (someFunction[Symbol.for('WS_PATCHED')]) console.log('This function is patched');"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Get the root fiber node | |
const main = document.querySelector("#__next"); | |
const fiberKey = Object.keys(main).find((key) => key.startsWith("__react")); | |
const fiberNode = main[fiberKey]; | |
const collectedGold = []; | |
const visitedNodes = new WeakSet(); | |
function isObjectOrArray(value) { | |
return (typeof value === "object" && value !== null) || Array.isArray(value); | |
} | |
function isStyleObject(obj) { | |
// Check if the object has typical style properties | |
return Object.keys(obj).some((key) => | |
["color", "fontSize", "margin", "padding", "display", "position"].includes( | |
key | |
) | |
); | |
} | |
function getElementInfo(node) { | |
if (node.stateNode && node.stateNode.tagName) { | |
return `<${node.stateNode.tagName.toLowerCase()}>`; | |
} | |
if (node.type && typeof node.type === "function") { | |
return node.type.name || "Anonymous Component"; | |
} | |
return "Unknown Element"; | |
} | |
function traverseFiberTree(node, path = "", depth = 0) { | |
if ( | |
!node || | |
typeof node !== "object" || | |
depth > 1000 || | |
visitedNodes.has(node) | |
) | |
return; | |
visitedNodes.add(node); | |
// Check memoizedProps | |
if (node.memoizedProps && typeof node.memoizedProps === "object") { | |
Object.entries(node.memoizedProps).forEach(([key, value]) => { | |
if ( | |
key !== "children" && | |
isObjectOrArray(value) && | |
!isStyleObject(value) | |
) { | |
const newPath = `${path}.memoizedProps.${key}`; | |
collectedGold.push({ | |
path: newPath, | |
value, | |
node, | |
}); | |
} | |
}); | |
} | |
// Traverse child | |
if (node.child) { | |
traverseFiberTree(node.child, `${path}.child`, depth + 1); | |
} | |
// Traverse sibling | |
if (node.sibling) { | |
traverseFiberTree(node.sibling, `${path}.sibling`, depth); | |
} | |
} | |
// Start the traversal | |
console.log("Starting enhanced fiber tree traversal:"); | |
traverseFiberTree(fiberNode); | |
// Log the collected "gold" | |
console.log( | |
"Collected valuable properties (objects and arrays, excluding styles):" | |
); | |
collectedGold.forEach(({ path, value, node }) => { | |
console.log("Value:", value); | |
}); | |
console.log(`Total valuable properties found: ${collectedGold.length}`); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Get the root fiber node | |
const main = document.querySelector("#__next"); | |
const fiberKey = Object.keys(main).find((key) => key.startsWith("__react")); | |
const fiberNode = main[fiberKey]; | |
const collectedGold = []; | |
const visitedNodes = new WeakSet(); | |
function isObjectOrArray(value) { | |
return (typeof value === "object" && value !== null) || Array.isArray(value); | |
} | |
function isStyleObject(obj) { | |
// Check if the object has typical style properties | |
return Object.keys(obj).some((key) => | |
["color", "fontSize", "margin", "padding", "display", "position"].includes( | |
key | |
) | |
); | |
} | |
function getElementInfo(node) { | |
if (node.stateNode && node.stateNode.tagName) { | |
return `<${node.stateNode.tagName.toLowerCase()}>`; | |
} | |
if (node.type && typeof node.type === "function") { | |
return node.type.name || "Anonymous Component"; | |
} | |
return "Unknown Element"; | |
} | |
function traverseFiberTree(node, path = "", depth = 0) { | |
if ( | |
!node || | |
typeof node !== "object" || | |
depth > 1000 || | |
visitedNodes.has(node) | |
) | |
return; | |
visitedNodes.add(node); | |
// Check memoizedProps | |
if (node.memoizedProps && typeof node.memoizedProps === "object") { | |
Object.entries(node.memoizedProps).forEach(([key, value]) => { | |
if ( | |
key !== "children" && | |
isObjectOrArray(value) && | |
!isStyleObject(value) | |
) { | |
const newPath = `${path}.memoizedProps.${key}`; | |
collectedGold.push({ | |
path: newPath, | |
value, | |
node, | |
}); | |
} | |
}); | |
} | |
// Traverse child | |
if (node.child) { | |
traverseFiberTree(node.child, `${path}.child`, depth + 1); | |
} | |
// Traverse sibling | |
if (node.sibling) { | |
traverseFiberTree(node.sibling, `${path}.sibling`, depth); | |
} | |
} | |
// Start the traversal | |
console.log("Starting enhanced fiber tree traversal:"); | |
traverseFiberTree(fiberNode); | |
// Log the collected "gold" | |
console.log( | |
"Collected valuable properties (objects and arrays, excluding styles):" | |
); | |
collectedGold.forEach(({ path, value, node }) => { | |
console.log("Value:", value); | |
}); | |
console.log(`Total valuable properties found: ${collectedGold.length}`); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Structured search criteria | |
const structuredSearchCriteria = [ | |
{ | |
label: "React Query", | |
criteria: [ | |
{ | |
client: { | |
queryCache: true, | |
}, | |
}, | |
], | |
}, | |
]; | |
// Flatten the criteria for use in the searchReactFiber function | |
const flattenedCriteria = structuredSearchCriteria.flatMap( | |
(group) => group.criteria | |
); | |
// Search configuration | |
const searchConfig = { | |
searchCriteria: flattenedCriteria, | |
maxDepth: 50, | |
stopAfterFirst: true, | |
searchPaths: ["memoizedProps", "memoizedState"], | |
mainSelector: "body", | |
callback: (matchingObjects) => { | |
matchingObjects.forEach(({ matchingObject, fiberNode, criteriaIndex }) => { | |
const matchedCriteria = flattenedCriteria[criteriaIndex]; | |
const matchedGroup = structuredSearchCriteria.find((group) => | |
group.criteria.some((c) => c === matchedCriteria) | |
); | |
console.log(`Found ${matchedGroup.label} pattern:`, matchingObject); | |
if (matchingObject.client.queryCache) { | |
const queryCache = matchingObject.client.queryCache; | |
console.log("QueryCache found: ", queryCache); | |
// objectentries to array of key value pairs | |
queryCache.queries?.forEach((query) => { | |
query.state.data && console.log("Query: ", query.state.data); | |
}); | |
} | |
// Force rerender the component to see the monkey patching in action | |
forceRerender(fiberNode); | |
}); | |
}, | |
}; | |
// Create a WeakSet to keep track of patched objects | |
const patchedObjects = new WeakSet(); | |
// Safe monkey patching function | |
function monkeyPatch(obj, prop, callback) { | |
// Check if the object has already been patched | |
if (patchedObjects.has(obj)) { | |
console.log( | |
`Object containing ${prop} has already been patched. Skipping.` | |
); | |
return; | |
} | |
const originalFunc = obj[prop]; | |
// Check if the function has already been patched | |
if (originalFunc.WS_PATCHED) { | |
console.log(`Function ${prop} has already been patched. Skipping.`); | |
return; | |
} | |
obj[prop] = function (...args) { | |
const result = originalFunc.apply(this, args); | |
// Log arguments | |
console.log(`${prop} called with args:`, args); | |
// Handle promises | |
if (result && typeof result.then === "function") { | |
result.then( | |
(value) => { | |
console.log(`${prop} resolved with:`, value); | |
callback(prop, args, value); | |
}, | |
(error) => { | |
console.log(`${prop} rejected with:`, error); | |
callback(prop, args, null, error); | |
} | |
); | |
} else { | |
console.log(`${prop} returned:`, result); | |
callback(prop, args, result); | |
} | |
return result; | |
}; | |
obj[prop].WS_PATCHED = true; | |
// Mark the object as patched | |
patchedObjects.add(obj); | |
} | |
// Main function to search the React Fiber tree | |
function searchReactFiber(config) { | |
const results = []; | |
const visitedNodes = new WeakSet(); | |
// Helper function to safely access object properties | |
function safelyAccessProperty(obj, prop) { | |
try { | |
return obj[prop]; | |
} catch (error) { | |
if (error instanceof DOMException && error.name === "SecurityError") { | |
return null; // Silently skip security-restricted properties | |
} | |
throw error; | |
} | |
} | |
// Check if an object contains all keys from the criteria | |
function isMatchingObject(obj, criteria) { | |
if (typeof obj !== "object" || obj === null) return false; | |
return Object.entries(criteria).every(([key, value]) => { | |
const objValue = safelyAccessProperty(obj, key); | |
if (objValue === null || objValue === undefined) return false; | |
if (typeof value === "object" && value !== null) { | |
return isMatchingObject(objValue, value); | |
} else if (value === true) { | |
return true; | |
} else { | |
return objValue === value; | |
} | |
}); | |
} | |
// Check if an object matches any of the criteria | |
function matchesAnyCriteria(obj, criteriaArray) { | |
for (let i = 0; i < criteriaArray.length; i++) { | |
if (isMatchingObject(obj, criteriaArray[i])) { | |
return { matched: true, index: i }; | |
} | |
} | |
return { matched: false, index: -1 }; | |
} | |
// Traverse the Fiber tree | |
function traverseFiberTree(startNode) { | |
const stack = [{ node: startNode, depth: 0 }]; | |
while (stack.length > 0) { | |
const { node, depth } = stack.pop(); | |
if ( | |
!node || | |
typeof node !== "object" || | |
depth > config.maxDepth || | |
visitedNodes.has(node) | |
) { | |
continue; | |
} | |
visitedNodes.add(node); | |
// Check if the node or its stateNode has already been patched | |
if ( | |
patchedObjects.has(node) || | |
(node.stateNode && patchedObjects.has(node.stateNode)) | |
) { | |
console.log("Skipping already patched node or its stateNode"); | |
continue; | |
} | |
// Check searchPaths for matching objects | |
for (const propName of config.searchPaths) { | |
const propValue = safelyAccessProperty(node, propName); | |
if (propValue && typeof propValue === "object") { | |
const match = matchesAnyCriteria(propValue, config.searchCriteria); | |
if (match.matched) { | |
results.push({ | |
matchingObject: propValue, | |
fiberNode: node, | |
criteriaIndex: match.index, | |
}); | |
if (config.stopAfterFirst) return results; | |
} | |
// Search nested objects in memoizedProps and memoizedState | |
if (propName === "memoizedProps" || propName === "memoizedState") { | |
searchNestedObjects(propValue, node); | |
} | |
} | |
} | |
// Add child and sibling nodes to the stack | |
const child = safelyAccessProperty(node, "child"); | |
if (child) stack.push({ node: child, depth: depth + 1 }); | |
const sibling = safelyAccessProperty(node, "sibling"); | |
if (sibling) stack.push({ node: sibling, depth }); | |
} | |
} | |
// Search nested objects within a node | |
function searchNestedObjects(obj, fiberNode) { | |
const stack = [obj]; | |
const visited = new WeakSet(); | |
while (stack.length > 0) { | |
const current = stack.pop(); | |
if ( | |
typeof current !== "object" || | |
current === null || | |
visited.has(current) | |
) { | |
continue; | |
} | |
visited.add(current); | |
const match = matchesAnyCriteria(current, config.searchCriteria); | |
if (match.matched) { | |
results.push({ | |
matchingObject: current, | |
fiberNode, | |
criteriaIndex: match.index, | |
}); | |
if (config.stopAfterFirst) return; | |
} | |
// Push all nested objects onto the stack | |
Object.values(current).forEach((value) => { | |
if ( | |
typeof value === "object" && | |
value !== null && | |
!visited.has(value) | |
) { | |
stack.push(value); | |
} | |
}); | |
} | |
} | |
// Get the root fiber node | |
const main = document.querySelector(config.mainSelector); | |
if (!main) { | |
console.warn( | |
`Main element not found with selector: ${config.mainSelector}` | |
); | |
return results; | |
} | |
const fiberKey = Object.keys(main).find((key) => key.startsWith("__react")); | |
if (!fiberKey) { | |
console.warn( | |
"React fiber key not found. This may not be a React application or the fiber structure has changed." | |
); | |
return results; | |
} | |
const fiberNode = safelyAccessProperty(main, fiberKey); | |
if (!fiberNode) { | |
console.warn("Unable to access fiber node. Skipping search."); | |
return results; | |
} | |
// Start the search | |
traverseFiberTree(fiberNode); | |
// Call the callback function if provided | |
if (typeof config.callback === "function") { | |
config.callback(results); | |
} | |
return results; | |
} | |
// Helper function to force rerender a component | |
function forceRerender(fiber) { | |
while (fiber && !fiber.stateNode?.forceUpdate) { | |
fiber = fiber.return; | |
} | |
if (fiber && fiber.stateNode) { | |
fiber.stateNode.forceUpdate(); | |
} | |
} | |
// Execute the search | |
console.time("Search time"); | |
const matchingObjects = searchReactFiber(searchConfig); | |
console.timeEnd("Search time"); | |
console.log(`Found ${matchingObjects.length} matching objects`); | |
// Apply monkey patching to found objects | |
console.log("Applying monkey patching to found objects..."); | |
matchingObjects.forEach(({ matchingObject, fiberNode, criteriaIndex }) => { | |
const matchedCriteria = flattenedCriteria[criteriaIndex]; | |
// Check if the object or its associated fiber node has already been patched | |
if ( | |
patchedObjects.has(matchingObject) || | |
patchedObjects.has(fiberNode) || | |
(fiberNode.stateNode && patchedObjects.has(fiberNode.stateNode)) | |
) { | |
console.log("Skipping already patched object or its associated fiber node"); | |
return; | |
} | |
Object.keys(matchedCriteria).forEach((key) => { | |
if ( | |
typeof matchingObject[key] === "function" && | |
!matchingObject[key].WS_PATCHED | |
) { | |
monkeyPatch(matchingObject, key, (funcName, args, result, error) => { | |
console.log(`Monkey patched function ${funcName} called:`, { | |
args, | |
result, | |
error, | |
}); | |
}); | |
} | |
}); | |
}); | |
console.log( | |
"Monkey patching completed. State management functions are now being monitored." | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Structured search criteria | |
const structuredSearchCriteria = [ | |
{ | |
label: "React Query", | |
criteria: [ | |
{ | |
client: { | |
queryCache: true, | |
}, | |
}, | |
], | |
}, | |
]; | |
// Flatten the criteria for use in the searchReactFiber function | |
const flattenedCriteria = structuredSearchCriteria.flatMap( | |
(group) => group.criteria | |
); | |
// Search configuration | |
const searchConfig = { | |
searchCriteria: flattenedCriteria, | |
maxDepth: 50, | |
stopAfterFirst: true, | |
searchPaths: ["memoizedProps", "memoizedState"], | |
mainSelector: "body", | |
callback: (matchingObjects) => { | |
matchingObjects.forEach(({ matchingObject, fiberNode, criteriaIndex }) => { | |
const matchedCriteria = flattenedCriteria[criteriaIndex]; | |
const matchedGroup = structuredSearchCriteria.find((group) => | |
group.criteria.some((c) => c === matchedCriteria) | |
); | |
console.log(`Found ${matchedGroup.label} pattern:`, matchingObject); | |
if (matchingObject.client.queryCache) { | |
const queryCache = matchingObject.client.queryCache; | |
console.log("QueryCache found: ", queryCache); | |
// objectentries to array of key value pairs | |
queryCache.queries?.forEach((query) => { | |
query.state.data && console.log("Query: ", query.state.data); | |
}); | |
} | |
// Force rerender the component to see the monkey patching in action | |
forceRerender(fiberNode); | |
}); | |
}, | |
}; | |
// Create a WeakSet to keep track of patched objects | |
const patchedObjects = new WeakSet(); | |
// Safe monkey patching function | |
function monkeyPatch(obj, prop, callback) { | |
// Check if the object has already been patched | |
if (patchedObjects.has(obj)) { | |
console.log( | |
`Object containing ${prop} has already been patched. Skipping.` | |
); | |
return; | |
} | |
const originalFunc = obj[prop]; | |
// Check if the function has already been patched | |
if (originalFunc.WS_PATCHED) { | |
console.log(`Function ${prop} has already been patched. Skipping.`); | |
return; | |
} | |
obj[prop] = function (...args) { | |
const result = originalFunc.apply(this, args); | |
// Log arguments | |
console.log(`${prop} called with args:`, args); | |
// Handle promises | |
if (result && typeof result.then === "function") { | |
result.then( | |
(value) => { | |
console.log(`${prop} resolved with:`, value); | |
callback(prop, args, value); | |
}, | |
(error) => { | |
console.log(`${prop} rejected with:`, error); | |
callback(prop, args, null, error); | |
} | |
); | |
} else { | |
console.log(`${prop} returned:`, result); | |
callback(prop, args, result); | |
} | |
return result; | |
}; | |
obj[prop].WS_PATCHED = true; | |
// Mark the object as patched | |
patchedObjects.add(obj); | |
} | |
// Main function to search the React Fiber tree | |
function searchReactFiber(config) { | |
const results = []; | |
const visitedNodes = new WeakSet(); | |
// Helper function to safely access object properties | |
function safelyAccessProperty(obj, prop) { | |
try { | |
return obj[prop]; | |
} catch (error) { | |
if (error instanceof DOMException && error.name === "SecurityError") { | |
return null; // Silently skip security-restricted properties | |
} | |
throw error; | |
} | |
} | |
// Check if an object contains all keys from the criteria | |
function isMatchingObject(obj, criteria) { | |
if (typeof obj !== "object" || obj === null) return false; | |
return Object.entries(criteria).every(([key, value]) => { | |
const objValue = safelyAccessProperty(obj, key); | |
if (objValue === null || objValue === undefined) return false; | |
if (typeof value === "object" && value !== null) { | |
return isMatchingObject(objValue, value); | |
} else if (value === true) { | |
return true; | |
} else { | |
return objValue === value; | |
} | |
}); | |
} | |
// Check if an object matches any of the criteria | |
function matchesAnyCriteria(obj, criteriaArray) { | |
for (let i = 0; i < criteriaArray.length; i++) { | |
if (isMatchingObject(obj, criteriaArray[i])) { | |
return { matched: true, index: i }; | |
} | |
} | |
return { matched: false, index: -1 }; | |
} | |
// Traverse the Fiber tree | |
function traverseFiberTree(startNode) { | |
const stack = [{ node: startNode, depth: 0 }]; | |
while (stack.length > 0) { | |
const { node, depth } = stack.pop(); | |
if ( | |
!node || | |
typeof node !== "object" || | |
depth > config.maxDepth || | |
visitedNodes.has(node) | |
) { | |
continue; | |
} | |
visitedNodes.add(node); | |
// Check if the node or its stateNode has already been patched | |
if ( | |
patchedObjects.has(node) || | |
(node.stateNode && patchedObjects.has(node.stateNode)) | |
) { | |
console.log("Skipping already patched node or its stateNode"); | |
continue; | |
} | |
// Check searchPaths for matching objects | |
for (const propName of config.searchPaths) { | |
const propValue = safelyAccessProperty(node, propName); | |
if (propValue && typeof propValue === "object") { | |
const match = matchesAnyCriteria(propValue, config.searchCriteria); | |
if (match.matched) { | |
results.push({ | |
matchingObject: propValue, | |
fiberNode: node, | |
criteriaIndex: match.index, | |
}); | |
if (config.stopAfterFirst) return results; | |
} | |
// Search nested objects in memoizedProps and memoizedState | |
if (propName === "memoizedProps" || propName === "memoizedState") { | |
searchNestedObjects(propValue, node); | |
} | |
} | |
} | |
// Add child and sibling nodes to the stack | |
const child = safelyAccessProperty(node, "child"); | |
if (child) stack.push({ node: child, depth: depth + 1 }); | |
const sibling = safelyAccessProperty(node, "sibling"); | |
if (sibling) stack.push({ node: sibling, depth }); | |
} | |
} | |
// Search nested objects within a node | |
function searchNestedObjects(obj, fiberNode) { | |
const stack = [obj]; | |
const visited = new WeakSet(); | |
while (stack.length > 0) { | |
const current = stack.pop(); | |
if ( | |
typeof current !== "object" || | |
current === null || | |
visited.has(current) | |
) { | |
continue; | |
} | |
visited.add(current); | |
const match = matchesAnyCriteria(current, config.searchCriteria); | |
if (match.matched) { | |
results.push({ | |
matchingObject: current, | |
fiberNode, | |
criteriaIndex: match.index, | |
}); | |
if (config.stopAfterFirst) return; | |
} | |
// Push all nested objects onto the stack | |
Object.values(current).forEach((value) => { | |
if ( | |
typeof value === "object" && | |
value !== null && | |
!visited.has(value) | |
) { | |
stack.push(value); | |
} | |
}); | |
} | |
} | |
// Get the root fiber node | |
const main = document.querySelector(config.mainSelector); | |
if (!main) { | |
console.warn( | |
`Main element not found with selector: ${config.mainSelector}` | |
); | |
return results; | |
} | |
const fiberKey = Object.keys(main).find((key) => key.startsWith("__react")); | |
if (!fiberKey) { | |
console.warn( | |
"React fiber key not found. This may not be a React application or the fiber structure has changed." | |
); | |
return results; | |
} | |
const fiberNode = safelyAccessProperty(main, fiberKey); | |
if (!fiberNode) { | |
console.warn("Unable to access fiber node. Skipping search."); | |
return results; | |
} | |
// Start the search | |
traverseFiberTree(fiberNode); | |
// Call the callback function if provided | |
if (typeof config.callback === "function") { | |
config.callback(results); | |
} | |
return results; | |
} | |
// Helper function to force rerender a component | |
function forceRerender(fiber) { | |
while (fiber && !fiber.stateNode?.forceUpdate) { | |
fiber = fiber.return; | |
} | |
if (fiber && fiber.stateNode) { | |
fiber.stateNode.forceUpdate(); | |
} | |
} | |
// Execute the search | |
console.time("Search time"); | |
const matchingObjects = searchReactFiber(searchConfig); | |
console.timeEnd("Search time"); | |
console.log(`Found ${matchingObjects.length} matching objects`); | |
// Apply monkey patching to found objects | |
console.log("Applying monkey patching to found objects..."); | |
matchingObjects.forEach(({ matchingObject, fiberNode, criteriaIndex }) => { | |
const matchedCriteria = flattenedCriteria[criteriaIndex]; | |
// Check if the object or its associated fiber node has already been patched | |
if ( | |
patchedObjects.has(matchingObject) || | |
patchedObjects.has(fiberNode) || | |
(fiberNode.stateNode && patchedObjects.has(fiberNode.stateNode)) | |
) { | |
console.log("Skipping already patched object or its associated fiber node"); | |
return; | |
} | |
Object.keys(matchedCriteria).forEach((key) => { | |
if ( | |
typeof matchingObject[key] === "function" && | |
!matchingObject[key].WS_PATCHED | |
) { | |
monkeyPatch(matchingObject, key, (funcName, args, result, error) => { | |
console.log(`Monkey patched function ${funcName} called:`, { | |
args, | |
result, | |
error, | |
}); | |
}); | |
} | |
}); | |
}); | |
console.log( | |
"Monkey patching completed. State management functions are now being monitored." | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Configuration object for the search | |
const searchConfig = { | |
searchCriteria: { user: { id: true, email: true } }, // What we're looking for | |
maxDepth: 50, // How deep to search in the tree | |
stopAfterFirst: false, // Whether to stop after finding the first match | |
searchPaths: ["memoizedProps", "memoizedState"], // Where to look in each node | |
mainSelector: "#__next", // The root element of our React app | |
callback: (matchingObjects) => { | |
matchingObjects.forEach(({ matchingObject, fiberNode }) => { | |
console.log("Found matching object:", matchingObject); | |
console.log(fiberNode) | |
// Uncomment the next line to force a rerender on each match | |
// forceRerender(fiberNode); | |
}); | |
} | |
}; | |
// Main function to search the React Fiber tree | |
function searchReactFiber(config) { | |
const results = []; | |
const visitedNodes = new WeakSet(); | |
// Helper function to safely access object properties | |
function safelyAccessProperty(obj, prop) { | |
try { | |
return obj[prop]; | |
} catch (error) { | |
if (error instanceof DOMException && error.name === "SecurityError") { | |
return null; // Silently skip security-restricted properties | |
} | |
throw error; | |
} | |
} | |
// Check if an object matches our search criteria | |
function isMatchingObject(obj, criteria) { | |
if (typeof obj !== "object" || obj === null) return false; | |
for (const [key, value] of Object.entries(criteria)) { | |
const objValue = safelyAccessProperty(obj, key); | |
if (objValue === null) return false; | |
if (typeof value === "object" && value !== null) { | |
if (!isMatchingObject(objValue, value)) return false; | |
} else if (value === true) { | |
if (objValue === undefined) return false; | |
} else { | |
if (objValue !== value) return false; | |
} | |
} | |
return true; | |
} | |
// Traverse the Fiber tree | |
function traverseFiberTree(startNode) { | |
const stack = [{ node: startNode, depth: 0 }]; | |
while (stack.length > 0) { | |
const { node, depth } = stack.pop(); | |
if (!node || typeof node !== "object" || depth > config.maxDepth || visitedNodes.has(node)) { | |
continue; | |
} | |
visitedNodes.add(node); | |
// Check searchPaths for matching objects | |
for (const propName of config.searchPaths) { | |
const propValue = safelyAccessProperty(node, propName); | |
if (propValue && typeof propValue === "object") { | |
if (isMatchingObject(propValue, config.searchCriteria)) { | |
results.push({ matchingObject: propValue, fiberNode: node }); | |
if (config.stopAfterFirst) return; | |
} | |
// Search nested objects in memoizedProps and memoizedState | |
if (propName === "memoizedProps" || propName === "memoizedState") { | |
searchNestedObjects(propValue, node); | |
} | |
} | |
} | |
// Add child and sibling nodes to the stack | |
const child = safelyAccessProperty(node, 'child'); | |
if (child) stack.push({ node: child, depth: depth + 1 }); | |
const sibling = safelyAccessProperty(node, 'sibling'); | |
if (sibling) stack.push({ node: sibling, depth }); | |
} | |
} | |
// Search nested objects within a node | |
function searchNestedObjects(obj, fiberNode) { | |
const stack = [obj]; | |
const visited = new WeakSet(); | |
while (stack.length > 0) { | |
const current = stack.pop(); | |
if (typeof current !== "object" || current === null || visited.has(current)) { | |
continue; | |
} | |
visited.add(current); | |
if (isMatchingObject(current, config.searchCriteria)) { | |
results.push({ matchingObject: current, fiberNode }); | |
if (config.stopAfterFirst) return; | |
} | |
// Search keys that are likely to contain relevant data | |
const keysToSearch = Object.keys(current).filter(key => | |
typeof current[key] === "object" && | |
current[key] !== null && | |
!Array.isArray(current[key]) && | |
key !== "$$typeof" && | |
!key.startsWith("_") | |
); | |
for (const key of keysToSearch) { | |
const value = safelyAccessProperty(current, key); | |
if (value !== null) stack.push(value); | |
} | |
} | |
} | |
// Get the root fiber node | |
const main = document.querySelector(config.mainSelector); | |
if (!main) { | |
console.warn(`Main element not found with selector: ${config.mainSelector}`); | |
return results; | |
} | |
const fiberKey = Object.keys(main).find((key) => key.startsWith("__react")); | |
if (!fiberKey) { | |
console.warn("React fiber key not found. This may not be a React application or the fiber structure has changed."); | |
return results; | |
} | |
const fiberNode = safelyAccessProperty(main, fiberKey); | |
if (!fiberNode) { | |
console.warn("Unable to access fiber node. Skipping search."); | |
return results; | |
} | |
// Start the search | |
traverseFiberTree(fiberNode); | |
// Call the callback function if provided | |
if (typeof config.callback === 'function') { | |
config.callback(results); | |
} | |
return results; | |
} | |
// Helper function to force rerender a component | |
function forceRerender(fiber) { | |
while (fiber && !fiber.stateNode?.forceUpdate) { | |
fiber = fiber.return; | |
} | |
if (fiber && fiber.stateNode) { | |
fiber.stateNode.forceUpdate(); | |
} | |
} | |
// Execute the search | |
console.time("Search time"); | |
const matchingObjects = searchReactFiber(searchConfig); | |
console.timeEnd("Search time"); | |
console.log(`Found ${matchingObjects.length} matching objects`); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment