@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
JavaScript
;
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);
}