diff --git a/bin/webpack.js b/bin/webpack.js index fead38b..10da782 100755 --- a/bin/webpack.js +++ b/bin/webpack.js @@ -44,7 +44,7 @@ const isInstalled = packageName => { do { try { if ( - fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory() + fs.statSync('/usr/share/nodejs/webpack-cli/').isDirectory() ) { return true; } @@ -62,7 +62,7 @@ const isInstalled = packageName => { */ const runCli = cli => { const path = require("path"); - const pkgPath = require.resolve(`${cli.package}/package.json`); + const pkgPath = require.resolve(`/usr/share/nodejs/${cli.package}/package.json`); // eslint-disable-next-line node/no-missing-require const pkg = require(pkgPath); // eslint-disable-next-line node/no-missing-require diff --git a/debian/changelog b/debian/changelog index 67eebb8..8b1bd02 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,20 @@ +node-webpack (5.76.1+dfsg1+~cs17.16.16-1deepin1) unstable; urgency=medium + + * Fix CVE-2025-68157, CVE-2025-68458: SSRF vulnerabilities in + HttpUriPlugin CVE-2025-68157: The HTTP(S) resolver enforces + allowedUris only for the initial URL, but does not re-validate + after following HTTP 30x redirects. This allows an import that + appears restricted to a trusted allow-list to be redirected to + URLs outside the allow-list. CVE-2025-68458: The HTTP(S) resolver + can be bypassed to fetch resources from hosts outside allowedUris + by using crafted URLs that include userinfo + (username:password@host). If allowedUris enforcement relies on a raw + string prefix check, a URL that looks allow-listed can pass + validation while the actual network request is sent to a different + authority/host. + + -- Yadd Fri, 08 May 2026 00:55:50 +0800 + node-webpack (5.76.1+dfsg1+~cs17.16.16-1) unstable; urgency=medium * Team upload diff --git a/debian/patches/CVE-2025-68157-68458.patch b/debian/patches/CVE-2025-68157-68458.patch new file mode 100644 index 0000000..d8bccda --- /dev/null +++ b/debian/patches/CVE-2025-68157-68458.patch @@ -0,0 +1,178 @@ +Description: Fix SSRF vulnerabilities in HttpUriPlugin + CVE-2025-68157: The HTTP(S) resolver enforces allowedUris only for the initial + URL, but does not re-validate after following HTTP 30x redirects. This allows + an import that appears restricted to a trusted allow-list to be redirected to + URLs outside the allow-list. + CVE-2025-68458: The HTTP(S) resolver can be bypassed to fetch resources from + hosts outside allowedUris by using crafted URLs that include userinfo + (username:password@host). If allowedUris enforcement relies on a raw string + prefix check, a URL that looks allow-listed can pass validation while the + actual network request is sent to a different authority/host. +Origin: upstream, https://github.com/webpack/webpack/commit/2179fdbcb and https://github.com/webpack/webpack/commit/c51007023 +Bug: https://github.com/webpack/webpack/security/advisories/GHSA-38r7-794h-5758 +Bug: https://github.com/webpack/webpack/security/advisories/GHSA-8fgc-7cc6-rx7x +Forwarded: not-needed +Last-Update: 2026-05-08 + +--- a/lib/schemes/HttpUriPlugin.js ++++ b/lib/schemes/HttpUriPlugin.js +@@ -20,6 +20,8 @@ + + const getHttp = memoize(() => require("http")); + const getHttps = memoize(() => require("https")); ++ ++const MAX_REDIRECTS = 5; + const proxyFetch = (request, proxy) => (url, options, callback) => { + const eventEmitter = new EventEmitter(); + const doRequest = socket => +@@ -139,6 +141,22 @@ + * @property {string} contentType + */ + ++/** ++ * Sanitize URL for inclusion in error messages ++ * @param {string} href URL string to sanitize ++ * @returns {string} sanitized URL text for logs/errors ++ */ ++const sanitizeUrlForError = href => { ++ try { ++ const u = new URL(href); ++ return u.protocol + "//" + u.host; ++ } catch (_err) { ++ return String(href) ++ .slice(0, 200) ++ .replace(/[\r\n]/g, ""); ++ } ++}; ++ + const areLockfileEntriesEqual = (a, b) => { + return ( + a.resolved === b.resolved && +@@ -503,17 +521,56 @@ + + for (const { scheme, fetch } of schemes) { + /** +- * ++ * Validate redirect location against allowedUris ++ * @param {string} location Location header value ++ * @param {string} base Current absolute URL ++ * @returns {string} absolute, validated redirect target ++ */ ++ const validateRedirectLocation = (location, base) => { ++ let nextUrl; ++ try { ++ nextUrl = new URL(location, base); ++ } catch (_err) { ++ throw new Error( ++ "Invalid redirect URL: " + sanitizeUrlForError(location) ++ ); ++ } ++ if (nextUrl.protocol !== "http:" && nextUrl.protocol !== "https:") { ++ throw new Error( ++ "Redirected URL uses disallowed protocol: " + sanitizeUrlForError(nextUrl.href) ++ ); ++ } ++ if (!isAllowed(nextUrl.href)) { ++ throw new Error( ++ nextUrl.href + " doesn't match the allowedUris policy after redirect. These URIs are allowed:\n" + ++ allowedUris.map(uri => " - " + uri).join("\n") ++ ); ++ } ++ return nextUrl.href; ++ }; ++ ++ /** + * @param {string} url URL + * @param {string} integrity integrity + * @param {function((Error | null)=, { entry: LockfileEntry, content: Buffer, storeLock: boolean }=): void} callback callback ++ * @param {number=} redirectCount number of followed redirects + */ +- const resolveContent = (url, integrity, callback) => { ++ const resolveContent = (url, integrity, callback, redirectCount = 0) => { + const handleResult = (err, result) => { + if (err) return callback(err); + if ("location" in result) { ++ // Validate redirect target before following ++ let absolute; ++ try { ++ absolute = validateRedirectLocation(result.location, url); ++ } catch (err_) { ++ return callback(err_); ++ } ++ if (redirectCount >= MAX_REDIRECTS) { ++ return callback(new Error("Too many redirects")); ++ } + return resolveContent( +- result.location, ++ absolute, + integrity, + (err, innerResult) => { + if (err) return callback(err); +@@ -522,7 +579,8 @@ + content: innerResult.content, + storeLock: innerResult.storeLock && result.storeLock + }); +- } ++ }, ++ redirectCount + 1 + ); + } else { + if ( +@@ -644,8 +702,17 @@ + res.statusCode >= 301 && + res.statusCode <= 308 + ) { ++ let absolute; ++ try { ++ absolute = new URL(location, url).href; ++ } catch (err) { ++ logger.log( ++ `GET ${url} [${res.statusCode}] -> ${String(location)} (rejected: ${err.message})` ++ ); ++ return callback(err); ++ } + const result = { +- location: new URL(location, url).href ++ location: absolute + }; + if ( + !cachedResult || +@@ -739,14 +806,35 @@ + (url, callback) => fetchContentRaw(url, undefined, callback) + ); + ++ /** ++ * Check if a URI is allowed ++ * @param {string} uri URI to check ++ * @returns {boolean} true when allowed, otherwise false ++ */ + const isAllowed = uri => { ++ let parsedUri; ++ try { ++ // Parse the URI to prevent userinfo bypass attacks ++ // (e.g., http://allowed@malicious/path where @malicious is the actual host) ++ parsedUri = new URL(uri); ++ } catch (_err) { ++ return false; ++ } + for (const allowed of allowedUris) { + if (typeof allowed === "string") { +- if (uri.startsWith(allowed)) return true; ++ let parsedAllowed; ++ try { ++ parsedAllowed = new URL(allowed); ++ } catch (_err) { ++ continue; ++ } ++ if (parsedUri.href.startsWith(parsedAllowed.href)) { ++ return true; ++ } + } else if (typeof allowed === "function") { +- if (allowed(uri)) return true; ++ if (allowed(parsedUri.href)) return true; + } else { +- if (allowed.test(uri)) return true; ++ if (allowed.test(parsedUri.href)) return true; + } + } + return false; diff --git a/debian/patches/series b/debian/patches/series index 16f26f4..33bef1d 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -4,3 +4,4 @@ webpack-cli-path.patch terser-webpack-plugin.patch fix-for-jest-29.patch fix-tsconfig.patch +CVE-2025-68157-68458.patch diff --git a/lib/optimize/ConcatenatedModule.js b/lib/optimize/ConcatenatedModule.js index 6d1a33b..1843c9a 100644 --- a/lib/optimize/ConcatenatedModule.js +++ b/lib/optimize/ConcatenatedModule.js @@ -6,7 +6,7 @@ "use strict"; const eslintScope = require("eslint-scope"); -const Referencer = require("eslint-scope/lib/referencer"); +const Referencer = eslintScope.Referencer; const { CachedSource, ConcatSource, diff --git a/lib/schemes/HttpUriPlugin.js b/lib/schemes/HttpUriPlugin.js index 1de8e1c..9650c10 100644 --- a/lib/schemes/HttpUriPlugin.js +++ b/lib/schemes/HttpUriPlugin.js @@ -20,6 +20,8 @@ const memoize = require("../util/memoize"); const getHttp = memoize(() => require("http")); const getHttps = memoize(() => require("https")); + +const MAX_REDIRECTS = 5; const proxyFetch = (request, proxy) => (url, options, callback) => { const eventEmitter = new EventEmitter(); const doRequest = socket => @@ -139,6 +141,22 @@ const parseCacheControl = (cacheControl, requestTime) => { * @property {string} contentType */ +/** + * Sanitize URL for inclusion in error messages + * @param {string} href URL string to sanitize + * @returns {string} sanitized URL text for logs/errors + */ +const sanitizeUrlForError = href => { + try { + const u = new URL(href); + return u.protocol + "//" + u.host; + } catch (_err) { + return String(href) + .slice(0, 200) + .replace(/[\r\n]/g, ""); + } +}; + const areLockfileEntriesEqual = (a, b) => { return ( a.resolved === b.resolved && @@ -503,17 +521,56 @@ class HttpUriPlugin { for (const { scheme, fetch } of schemes) { /** - * + * Validate redirect location against allowedUris + * @param {string} location Location header value + * @param {string} base Current absolute URL + * @returns {string} absolute, validated redirect target + */ + const validateRedirectLocation = (location, base) => { + let nextUrl; + try { + nextUrl = new URL(location, base); + } catch (_err) { + throw new Error( + "Invalid redirect URL: " + sanitizeUrlForError(location) + ); + } + if (nextUrl.protocol !== "http:" && nextUrl.protocol !== "https:") { + throw new Error( + "Redirected URL uses disallowed protocol: " + sanitizeUrlForError(nextUrl.href) + ); + } + if (!isAllowed(nextUrl.href)) { + throw new Error( + nextUrl.href + " doesn't match the allowedUris policy after redirect. These URIs are allowed:\n" + + allowedUris.map(uri => " - " + uri).join("\n") + ); + } + return nextUrl.href; + }; + + /** * @param {string} url URL * @param {string} integrity integrity * @param {function((Error | null)=, { entry: LockfileEntry, content: Buffer, storeLock: boolean }=): void} callback callback + * @param {number=} redirectCount number of followed redirects */ - const resolveContent = (url, integrity, callback) => { + const resolveContent = (url, integrity, callback, redirectCount = 0) => { const handleResult = (err, result) => { if (err) return callback(err); if ("location" in result) { + // Validate redirect target before following + let absolute; + try { + absolute = validateRedirectLocation(result.location, url); + } catch (err_) { + return callback(err_); + } + if (redirectCount >= MAX_REDIRECTS) { + return callback(new Error("Too many redirects")); + } return resolveContent( - result.location, + absolute, integrity, (err, innerResult) => { if (err) return callback(err); @@ -522,7 +579,8 @@ class HttpUriPlugin { content: innerResult.content, storeLock: innerResult.storeLock && result.storeLock }); - } + }, + redirectCount + 1 ); } else { if ( @@ -644,8 +702,17 @@ class HttpUriPlugin { res.statusCode >= 301 && res.statusCode <= 308 ) { + let absolute; + try { + absolute = new URL(location, url).href; + } catch (err) { + logger.log( + `GET ${url} [${res.statusCode}] -> ${String(location)} (rejected: ${err.message})` + ); + return callback(err); + } const result = { - location: new URL(location, url).href + location: absolute }; if ( !cachedResult || @@ -739,14 +806,35 @@ class HttpUriPlugin { (url, callback) => fetchContentRaw(url, undefined, callback) ); + /** + * Check if a URI is allowed + * @param {string} uri URI to check + * @returns {boolean} true when allowed, otherwise false + */ const isAllowed = uri => { + let parsedUri; + try { + // Parse the URI to prevent userinfo bypass attacks + // (e.g., http://allowed@malicious/path where @malicious is the actual host) + parsedUri = new URL(uri); + } catch (_err) { + return false; + } for (const allowed of allowedUris) { if (typeof allowed === "string") { - if (uri.startsWith(allowed)) return true; + let parsedAllowed; + try { + parsedAllowed = new URL(allowed); + } catch (_err) { + continue; + } + if (parsedUri.href.startsWith(parsedAllowed.href)) { + return true; + } } else if (typeof allowed === "function") { - if (allowed(uri)) return true; + if (allowed(parsedUri.href)) return true; } else { - if (allowed.test(uri)) return true; + if (allowed.test(parsedUri.href)) return true; } } return false; diff --git a/package.json b/package.json index 5e3ff54..369b448 100644 --- a/package.json +++ b/package.json @@ -189,6 +189,10 @@ ] }, "jest": { + "snapshotFormat": { + "escapeString": true, + "printBasicPrototype": true + }, "forceExit": true, "setupFilesAfterEnv": [ "/test/setupTestFramework.js" diff --git a/terser-webpack-plugin/src/utils.js b/terser-webpack-plugin/src/utils.js index 55ebcea..e1ffc95 100644 --- a/terser-webpack-plugin/src/utils.js +++ b/terser-webpack-plugin/src/utils.js @@ -264,8 +264,11 @@ async function terserMinify( // Let terser generate a SourceMap if (sourceMap) { - // @ts-ignore - terserOptions.sourceMap = { asObject: true }; + const pkg = require("terser/package.json") + if (parseInt(pkg.version) > 4) { + // @ts-ignore + terserOptions.sourceMap = { asObject: true }; + } } /** @type {ExtractedComments} */ @@ -299,7 +302,11 @@ async function terserMinify( } const [[filename, code]] = Object.entries(input); - const result = await minify({ [filename]: code }, terserOptions); + let result = minify({ [filename]: code }, terserOptions); + + if (result instanceof Promise) { + result = await result + } return { code: /** @type {string} **/ (result.code), diff --git a/test/setupTestFramework.js b/test/setupTestFramework.js index 5dab1d8..713f586 100644 --- a/test/setupTestFramework.js +++ b/test/setupTestFramework.js @@ -130,6 +130,6 @@ if (process.env.DEBUG_INFO) { // Workaround for a memory leak in wabt // It leaks an Error object on construction // so it leaks the whole stack trace -require("wast-loader"); +//require("wast-loader"); process.removeAllListeners("uncaughtException"); process.removeAllListeners("unhandledRejection"); diff --git a/webpack-cli/packages/webpack-cli/src/webpack-cli.ts b/webpack-cli/packages/webpack-cli/src/webpack-cli.ts index 72eea48..fd3cb5a 100644 --- a/webpack-cli/packages/webpack-cli/src/webpack-cli.ts +++ b/webpack-cli/packages/webpack-cli/src/webpack-cli.ts @@ -62,6 +62,9 @@ const envinfo = require("envinfo"); const WEBPACK_PACKAGE = process.env.WEBPACK_PACKAGE || "webpack"; const WEBPACK_DEV_SERVER_PACKAGE = process.env.WEBPACK_DEV_SERVER_PACKAGE || "webpack-dev-server"; +// @ts-ignore +const isDebianManaged = () => Boolean(process.env.DEB_HOST_ARCH) + interface Information { Binaries?: string[]; Browsers?: string[]; @@ -189,7 +192,9 @@ class WebpackCLI implements IWebpackCLI { if (!availableInstallers.length) { this.logger.error("No package manager found."); - process.exit(2); + if (!isDebianManaged()) { + process.exit(2); + } } return availableInstallers; @@ -244,7 +249,9 @@ class WebpackCLI implements IWebpackCLI { } catch (e) { this.logger.error("No package manager found."); - process.exit(2); + if (!isDebianManaged()) { + process.exit(2); + } } } @@ -254,7 +261,9 @@ class WebpackCLI implements IWebpackCLI { if (!packageManager) { this.logger.error("Can't find package manager"); - process.exit(2); + if (!isDebianManaged()) { + process.exit(2); + } } if (options.preMessage) { @@ -528,6 +537,10 @@ class WebpackCLI implements IWebpackCLI { skipInstallation = true; } + if (!isDebianManaged()) { + skipInstallation = true + } + if (skipInstallation) { continue; } diff --git a/webpack-cli/tsconfig.json b/webpack-cli/tsconfig.json index 796ecc1..6566ed8 100644 --- a/webpack-cli/tsconfig.json +++ b/webpack-cli/tsconfig.json @@ -1,5 +1,5 @@ { - "exclude": ["node_modules", "lib", "__tests__"], + "exclude": ["lib", "__tests__"], "files": [], "compilerOptions": { "skipLibCheck": true,