From e23ccb7ff77c1706c85bec4a604d1e8bc3d31605 Mon Sep 17 00:00:00 2001 From: Oleg Chernov Date: Mon, 6 Apr 2026 00:50:23 +0300 Subject: [PATCH] fix: lazy PKG_NATIVE_CACHE_PATH eval + integrity check for cached .node files Two improvements to native addon extraction in process.dlopen: 1. Read PKG_NATIVE_CACHE_PATH lazily inside process.dlopen instead of capturing it once at bootstrap time. This allows applications to set process.env.PKG_NATIVE_CACHE_PATH at runtime (e.g. in an init script) before any native addon is loaded, which was previously impossible because the IIFE captured the value before any user code ran. 2. Add SHA-256 integrity verification for cached .node files in the single-file code path (the else branch). The node_modules path already had hash verification via copyFolderRecursiveSync, but the single-file path only checked fs.existsSync. A tampered or corrupted cached file is now detected and re-extracted from the snapshot. This closes a privilege escalation vector where native addons are extracted to user-writable directories (e.g. ~/.cache/pkg/) and can be replaced with malicious .node files that execute at the privilege level of the packaged application. Co-Authored-By: Claude Opus 4.6 --- prelude/bootstrap.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/prelude/bootstrap.js b/prelude/bootstrap.js index 8a8ee86f..4e8d26b6 100644 --- a/prelude/bootstrap.js +++ b/prelude/bootstrap.js @@ -2187,7 +2187,10 @@ function payloadFileSync(pointer) { // - Windows: C:\Users\John\.cache // Custom example: /opt/myapp/cache or C:\myapp\cache // Native addons will be extracted to: /pkg/ - const PKG_NATIVE_CACHE_BASE = + // + // Note: We capture the initial value as the default, but re-read process.env inside + // process.dlopen so that runtime changes to PKG_NATIVE_CACHE_PATH take effect. + const PKG_NATIVE_CACHE_DEFAULT = process.env.PKG_NATIVE_CACHE_PATH || path.join(homedir(), '.cache'); function revertMakingLong(f) { @@ -2209,7 +2212,9 @@ function payloadFileSync(pointer) { // the hash is needed to be sure we reload the module in case it changes const hash = createHash('sha256').update(moduleContent).digest('hex'); - const tmpFolder = path.join(PKG_NATIVE_CACHE_BASE, 'pkg', hash); + // Re-read PKG_NATIVE_CACHE_PATH at each call so runtime changes take effect + const cacheBase = process.env.PKG_NATIVE_CACHE_PATH || PKG_NATIVE_CACHE_DEFAULT; + const tmpFolder = path.join(cacheBase, 'pkg', hash); fs.mkdirSync(tmpFolder, { recursive: true }); @@ -2237,7 +2242,17 @@ function payloadFileSync(pointer) { } else { const tmpModulePath = path.join(tmpFolder, moduleBaseName); - if (!fs.existsSync(tmpModulePath)) { + if (fs.existsSync(tmpModulePath)) { + // Verify cached file integrity against snapshot content. + // The folder name encodes the expected hash, but the file inside could + // have been replaced (e.g. by a local user to inject malicious code). + const cachedContent = fs.readFileSync(tmpModulePath); + const cachedHash = createHash('sha256').update(cachedContent).digest('hex'); + if (cachedHash !== hash) { + // Cached file was tampered with or corrupted — re-extract from snapshot + fs.copyFileSync(modulePath, tmpModulePath); + } + } else { fs.copyFileSync(modulePath, tmpModulePath); }