UNPKG

@aikidosec/firewall

Version:

Zen by Aikido is an embedded Web Application Firewall that autonomously protects Node.js apps against common and critical attacks

259 lines (258 loc) 10.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.wrapRequire = wrapRequire; exports.setPackagesToPatch = setPackagesToPatch; exports.setBuiltinModulesToPatch = setBuiltinModulesToPatch; exports.getOriginalRequire = getOriginalRequire; exports.__internalRewritePackageName = __internalRewritePackageName; /* eslint-disable max-lines-per-function */ const mod = require("module"); const isBuiltinModule_1 = require("./isBuiltinModule"); const getModuleInfoFromPath_1 = require("./getModuleInfoFromPath"); const satisfiesVersion_1 = require("../../helpers/satisfiesVersion"); const removeNodePrefix_1 = require("../../helpers/removeNodePrefix"); const isMainJsFile_1 = require("./isMainJsFile"); const AgentSingleton_1 = require("../AgentSingleton"); const originalRequire = mod.prototype.require; let isRequireWrapped = false; let packages = []; let builtinModules = []; let pkgCache = new Map(); let builtinCache = new Map(); /** * Wraps the require function to intercept require calls. * This function makes sure that the require function is only wrapped once. */ function wrapRequire() { if (isRequireWrapped) { return; } // @ts-expect-error Not included in the Node.js types if (typeof mod._resolveFilename !== "function") { throw new Error(`Could not find the _resolveFilename function in node:module using Node.js version ${process.version}`); } // Prevent wrapping the require function multiple times isRequireWrapped = true; mod.prototype.require = function wrapped() { // eslint-disable-next-line prefer-rest-params return patchedRequire.call(this, arguments); }; // Wrap process.getBuiltinModule, which allows requiring builtin modules (since Node.js v22.3.0) if (typeof process.getBuiltinModule === "function") { process.getBuiltinModule = function wrappedGetBuiltinModule() { // eslint-disable-next-line prefer-rest-params return patchedRequire.call(this, arguments); }; } } /** * Update the list of external packages that should be patched. */ function setPackagesToPatch(packagesToPatch) { packages = packagesToPatch; // Reset cache pkgCache = new Map(); } /** * Update the list of builtin modules that should be patched. */ function setBuiltinModulesToPatch(builtinModulesToPatch) { builtinModules = builtinModulesToPatch; // Reset cache builtinCache = new Map(); } /** * Our custom require function that intercepts require calls. */ function patchedRequire(args) { var _a; // Apply the original require function const originalExports = originalRequire.apply(this, args); if (!args.length || typeof args[0] !== "string") { return originalExports; } /** * Parameter that is passed to the require function * Can be a module name, a relative / absolute path */ const id = args[0]; try { // Check if it's a builtin module // They are easier to patch (no file patching) // Separate handling for builtin modules improves the performance if ((0, isBuiltinModule_1.isBuiltinModule)(id)) { // Call function for patching builtin modules with the same context (this) return patchBuiltinModule.call(this, id, originalExports); } // Call function for patching external packages return patchPackage.call(this, id, originalExports); } catch (error) { if (error instanceof Error) { (_a = (0, AgentSingleton_1.getInstance)()) === null || _a === void 0 ? void 0 : _a.onFailedToWrapModule(id, error); } return originalExports; } } /** * Run all require interceptors for the builtin module and cache the result. */ function patchBuiltinModule(id, originalExports) { const moduleName = (0, removeNodePrefix_1.removeNodePrefix)(id); // Check if already cached if (builtinCache.has(moduleName)) { return builtinCache.get(moduleName); } // Check if we want to patch this builtin module const matchingBuiltins = builtinModules.filter((m) => m.getName() === moduleName); // We don't want to patch this builtin module if (!matchingBuiltins.length) { return originalExports; } // Get interceptors from all matching builtin modules const interceptors = matchingBuiltins .map((m) => m.getRequireInterceptors()) .flat(); return executeInterceptors(interceptors, originalExports, builtinCache, moduleName, { name: moduleName, type: "builtin", }); } /** * Run all require interceptors for the package and cache the result. * Also checks the package versions. Not used for builtin modules. */ function patchPackage(id, originalExports) { // Get the full filepath of the required js file // @ts-expect-error Not included in the Node.js types const filename = mod._resolveFilename(id, this); if (!filename) { throw new Error("Could not resolve filename using _resolveFilename"); } // Ignore .json files if (filename.endsWith(".json")) { return originalExports; } // Check if cache has the filename if (pkgCache.has(filename)) { return pkgCache.get(filename); } // Parses the filename to extract the module name, the base dir of the module and the relative path of the included file const pathInfo = (0, getModuleInfoFromPath_1.getModuleInfoFromPath)(filename); if (!pathInfo) { // Can happen if the package is not inside a node_modules folder, like the dev build of our library itself return originalExports; } const moduleName = pathInfo.name; // Get all versioned packages for the module name const versionedPackagesToInstrument = packages .filter((pkg) => pkg.getName() === moduleName) .map((pkg) => pkg.getVersions()) .flat(); // Read the package.json of the required package let packageJson; try { packageJson = originalRequire(`${pathInfo.base}/package.json`); } catch { return originalExports; } // Get the version of the installed package const installedPkgVersion = packageJson.version; if (!installedPkgVersion) { throw new Error(`Could not get installed package version for ${moduleName}`); } const agent = (0, AgentSingleton_1.getInstance)(); agent === null || agent === void 0 ? void 0 : agent.onPackageRequired(moduleName, installedPkgVersion); // We don't want to patch this package because we do not have any hooks for it if (!versionedPackagesToInstrument.length) { return originalExports; } // Check if the installed package version is supported (get all matching versioned packages) const matchingVersionedPackages = versionedPackagesToInstrument.filter((pkg) => (0, satisfiesVersion_1.satisfiesVersion)(pkg.getRange(), installedPkgVersion)); // Report to the agent that the package was wrapped or not if it's version is not supported agent === null || agent === void 0 ? void 0 : agent.onPackageWrapped(moduleName, { version: installedPkgVersion, supported: !!matchingVersionedPackages.length, }); if (!matchingVersionedPackages.length) { // We don't want to patch this package version return originalExports; } // Check if the required file is the main file of the package or another js file inside the package const isMainFile = (0, isMainJsFile_1.isMainJsFile)(pathInfo, id, filename, packageJson); let interceptors = []; if (isMainFile) { interceptors = matchingVersionedPackages .map((pkg) => pkg.getRequireInterceptors()) .flat(); } else { // If it's not the main file, we want to check if the want to patch the required file interceptors = matchingVersionedPackages .map((pkg) => pkg.getRequireFileInterceptor(pathInfo.path) || []) .flat(); } return executeInterceptors(interceptors, originalExports, pkgCache, filename, { name: pathInfo.name, version: installedPkgVersion, type: "external", path: { base: pathInfo.base, relative: pathInfo.path, }, }); } /** * Executes the provided require interceptor functions and sets the cache. */ function executeInterceptors(interceptors, exports, cache, cacheKey, wrapPackageInfo) { var _a; // Cache because we need to prevent this called again if module is imported inside interceptors cache.set(cacheKey, exports); // Return early if no interceptors if (!interceptors.length) { return exports; } // Foreach interceptor function for (const interceptor of interceptors) { // If one interceptor fails, we don't want to stop the other interceptors try { const returnVal = interceptor(exports, wrapPackageInfo); // If the interceptor returns a value, we want to use this value as the new exports if (typeof returnVal !== "undefined") { exports = returnVal; } } catch (error) { if (error instanceof Error) { (_a = (0, AgentSingleton_1.getInstance)()) === null || _a === void 0 ? void 0 : _a.onFailedToWrapModule(wrapPackageInfo.name, error); } } } // Finally cache the result cache.set(cacheKey, exports); return exports; } /** * Returns the unwrapped require function. */ function getOriginalRequire() { return originalRequire; } // In order to support multiple versions of the same package, we need to rewrite the package name // e.g. In our sources and sinks, we use the real package name `hooks.addPackage("undici")` // but in the tests we want to `require("undici-v6")` instead of `require("undici")` function __internalRewritePackageName(packageName, aliasForTesting) { if (!isRequireWrapped) { throw new Error("Start the agent before calling __internalRewritePackageName(..)"); } if (packages.length === 0) { throw new Error("No packages to patch"); } const pkg = packages.find((pkg) => pkg.getName() === packageName); if (!pkg) { throw new Error(`Could not find package ${packageName}`); } pkg.setName(aliasForTesting); }