From 3bd962a6d7f61239c020e2dbbeb7341e5b842dd1 Mon Sep 17 00:00:00 2001
From: WXL <wl_5969728@163.com>
Date: 星期二, 21 四月 2026 11:46:41 +0800
Subject: [PATCH] 推送

---
 node_modules/webpack/lib/schemes/HttpUriPlugin.js |  241 +++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 199 insertions(+), 42 deletions(-)

diff --git a/node_modules/webpack/lib/schemes/HttpUriPlugin.js b/node_modules/webpack/lib/schemes/HttpUriPlugin.js
index 366c79e..7ec496d 100644
--- a/node_modules/webpack/lib/schemes/HttpUriPlugin.js
+++ b/node_modules/webpack/lib/schemes/HttpUriPlugin.js
@@ -14,7 +14,6 @@
 	createInflate
 } = require("zlib");
 const NormalModule = require("../NormalModule");
-const createSchemaValidation = require("../util/create-schema-validation");
 const createHash = require("../util/createHash");
 const { dirname, join, mkdirp } = require("../util/fs");
 const memoize = require("../util/memoize");
@@ -34,15 +33,28 @@
 const getHttp = memoize(() => require("http"));
 const getHttps = memoize(() => require("https"));
 
+const MAX_REDIRECTS = 5;
+
+/** @typedef {(url: URL, requestOptions: RequestOptions, callback: (incomingMessage: IncomingMessage) => void) => EventEmitter} Fetch */
+
 /**
+ * Defines the events map type used by this module.
+ * @typedef {object} EventsMap
+ * @property {[Error]} error
+ */
+
+/**
+ * Returns fn.
  * @param {typeof import("http") | typeof import("https")} request request
  * @param {string | URL | undefined} proxy proxy
- * @returns {(url: URL, requestOptions: RequestOptions, callback: (incomingMessage: IncomingMessage) => void) => EventEmitter} fn
+ * @returns {Fetch} fn
  */
 const proxyFetch = (request, proxy) => (url, options, callback) => {
+	/** @type {EventEmitter<EventsMap>} */
 	const eventEmitter = new EventEmitter();
 
 	/**
+	 * Processes the provided socket.
 	 * @param {Socket=} socket socket
 	 * @returns {void}
 	 */
@@ -66,6 +78,13 @@
 				if (res.statusCode === 200) {
 					// connected to proxy server
 					doRequest(socket);
+				} else {
+					eventEmitter.emit(
+						"error",
+						new Error(
+							`Failed to connect to proxy server "${proxy}": ${res.statusCode} ${res.statusMessage}`
+						)
+					);
 				}
 			})
 			.on("error", (err) => {
@@ -88,25 +107,16 @@
 /** @type {InProgressWriteItem[] | undefined} */
 let inProgressWrite;
 
-const validate = createSchemaValidation(
-	require("../../schemas/plugins/schemes/HttpUriPlugin.check"),
-	() => require("../../schemas/plugins/schemes/HttpUriPlugin.json"),
-	{
-		name: "Http Uri Plugin",
-		baseDataPath: "options"
-	}
-);
-
 /**
+ * Returns safe path.
  * @param {string} str path
  * @returns {string} safe path
  */
 const toSafePath = (str) =>
-	str
-		.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "")
-		.replace(/[^a-zA-Z0-9._-]+/g, "_");
+	str.replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, "").replace(/[^a-z0-9._-]+/gi, "_");
 
 /**
+ * Returns integrity.
  * @param {Buffer} content content
  * @returns {string} integrity
  */
@@ -118,6 +128,7 @@
 };
 
 /**
+ * Returns true, if integrity matches.
  * @param {Buffer} content content
  * @param {string} integrity integrity
  * @returns {boolean} true, if integrity matches
@@ -128,6 +139,7 @@
 };
 
 /**
+ * Parses key value pairs.
  * @param {string} str input
  * @returns {Record<string, string>} parsed
  */
@@ -150,6 +162,7 @@
 };
 
 /**
+ * Parses cache control.
  * @param {string | undefined} cacheControl Cache-Control header
  * @param {number} requestTime timestamp of request
  * @returns {{ storeCache: boolean, storeLock: boolean, validUntil: number }} Logic for storing in cache and lockfile cache
@@ -177,6 +190,7 @@
 };
 
 /**
+ * Defines the lockfile entry type used by this module.
  * @typedef {object} LockfileEntry
  * @property {string} resolved
  * @property {string} integrity
@@ -184,6 +198,7 @@
  */
 
 /**
+ * Are lockfile entries equal.
  * @param {LockfileEntry} a first lockfile entry
  * @param {LockfileEntry} b second lockfile entry
  * @returns {boolean} true when equal, otherwise false
@@ -194,20 +209,39 @@
 	a.contentType === b.contentType;
 
 /**
+ * Returns , integrity: ${string}, contentType: ${string}`} stringified entry.
  * @param {LockfileEntry} entry lockfile entry
  * @returns {`resolved: ${string}, integrity: ${string}, contentType: ${string}`} stringified entry
  */
 const entryToString = (entry) =>
 	`resolved: ${entry.resolved}, integrity: ${entry.integrity}, contentType: ${entry.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, "");
+	}
+};
+
 class Lockfile {
 	constructor() {
+		/** @type {number} */
 		this.version = 1;
 		/** @type {Map<string, LockfileEntry | "ignore" | "no-cache">} */
 		this.entries = new Map();
 	}
 
 	/**
+	 * Parses the provided source and updates the parser state.
 	 * @param {string} content content of the lockfile
 	 * @returns {Lockfile} lockfile
 	 */
@@ -235,6 +269,7 @@
 	}
 
 	/**
+	 * Returns a string representation.
 	 * @returns {string} stringified lockfile
 	 */
 	toString() {
@@ -259,16 +294,19 @@
 }
 
 /**
+ * Defines the fn without key callback type used by this module.
  * @template R
  * @typedef {(err: Error | null, result?: R) => void}  FnWithoutKeyCallback
  */
 
 /**
+ * Defines the fn without key type used by this module.
  * @template R
  * @typedef {(callback: FnWithoutKeyCallback<R>) => void} FnWithoutKey
  */
 
 /**
+ * Caches d without key.
  * @template R
  * @param {FnWithoutKey<R>} fn function
  * @returns {FnWithoutKey<R>} cached function
@@ -302,31 +340,36 @@
 };
 
 /**
+ * Defines the fn with key callback type used by this module.
  * @template R
  * @typedef {(err: Error | null, result?: R) => void} FnWithKeyCallback
  */
 
 /**
+ * Defines the fn with key type used by this module.
  * @template T
  * @template R
  * @typedef {(item: T, callback: FnWithKeyCallback<R>) => void} FnWithKey
  */
 
 /**
+ * Returns } cached function.
  * @template T
  * @template R
  * @param {FnWithKey<T, R>} fn function
  * @param {FnWithKey<T, R>=} forceFn function for the second try
- * @returns {(FnWithKey<T, R>) & { force: FnWithKey<T, R> }} cached function
+ * @returns {FnWithKey<T, R> & { force: FnWithKey<T, R> }} cached function
  */
 const cachedWithKey = (fn, forceFn = fn) => {
 	/**
+	 * Defines the cache entry type used by this module.
 	 * @template R
 	 * @typedef {{ result?: R, error?: Error, callbacks?: FnWithKeyCallback<R>[], force?: true }} CacheEntry
 	 */
 	/** @type {Map<T, CacheEntry<R>>} */
 	const cache = new Map();
 	/**
+	 * Processes the provided arg.
 	 * @param {T} arg arg
 	 * @param {FnWithKeyCallback<R>} callback callback
 	 * @returns {void}
@@ -359,6 +402,7 @@
 		});
 	};
 	/**
+	 * Processes the provided arg.
 	 * @param {T} arg arg
 	 * @param {FnWithKeyCallback<R>} callback callback
 	 * @returns {void}
@@ -395,12 +439,14 @@
 };
 
 /**
+ * Defines the lockfile cache type used by this module.
  * @typedef {object} LockfileCache
  * @property {Lockfile} lockfile lockfile
  * @property {Snapshot} snapshot snapshot
  */
 
 /**
+ * Defines the resolve content result type used by this module.
  * @typedef {object} ResolveContentResult
  * @property {LockfileEntry} entry lockfile entry
  * @property {Buffer} content content
@@ -412,30 +458,44 @@
 /** @typedef {FetchResultMeta & { entry: LockfileEntry, content: Buffer }} ContentFetchResult */
 /** @typedef {RedirectFetchResult | ContentFetchResult} FetchResult */
 
+/** @typedef {(uri: string) => boolean} AllowedUriFn */
+
 const PLUGIN_NAME = "HttpUriPlugin";
 
 class HttpUriPlugin {
 	/**
+	 * Creates an instance of HttpUriPlugin.
 	 * @param {HttpUriPluginOptions} options options
 	 */
 	constructor(options) {
-		validate(options);
-		this._lockfileLocation = options.lockfileLocation;
-		this._cacheLocation = options.cacheLocation;
-		this._upgrade = options.upgrade;
-		this._frozen = options.frozen;
-		this._allowedUris = options.allowedUris;
-		this._proxy = options.proxy;
+		/** @type {HttpUriPluginOptions} */
+		this.options = options;
 	}
 
 	/**
-	 * Apply the plugin
+	 * Applies the plugin by registering its hooks on the compiler.
 	 * @param {Compiler} compiler the compiler instance
 	 * @returns {void}
 	 */
 	apply(compiler) {
+		compiler.hooks.validate.tap(PLUGIN_NAME, () => {
+			compiler.validate(
+				() => require("../../schemas/plugins/schemes/HttpUriPlugin.json"),
+				this.options,
+				{
+					name: "Http Uri Plugin",
+					baseDataPath: "options"
+				},
+				(options) =>
+					require("../../schemas/plugins/schemes/HttpUriPlugin.check")(options)
+			);
+		});
+
 		const proxy =
-			this._proxy || process.env.http_proxy || process.env.HTTP_PROXY;
+			this.options.proxy || process.env.http_proxy || process.env.HTTP_PROXY;
+		/**
+		 * @type {{ scheme: "http" | "https", fetch: Fetch }[]}
+		 */
 		const schemes = [
 			{
 				scheme: "http",
@@ -459,7 +519,7 @@
 				const logger = compilation.getLogger(`webpack.${PLUGIN_NAME}`);
 				/** @type {string} */
 				const lockfileLocation =
-					this._lockfileLocation ||
+					this.options.lockfileLocation ||
 					join(
 						intermediateFs,
 						compiler.context,
@@ -469,21 +529,22 @@
 					);
 				/** @type {string | false} */
 				const cacheLocation =
-					this._cacheLocation !== undefined
-						? this._cacheLocation
+					this.options.cacheLocation !== undefined
+						? this.options.cacheLocation
 						: `${lockfileLocation}.data`;
-				const upgrade = this._upgrade || false;
-				const frozen = this._frozen || false;
+				const upgrade = this.options.upgrade || false;
+				const frozen = this.options.frozen || false;
 				const hashFunction = "sha512";
 				const hashDigest = "hex";
 				const hashDigestLength = 20;
-				const allowedUris = this._allowedUris;
+				const allowedUris = this.options.allowedUris;
 
 				let warnedAboutEol = false;
 
 				/** @type {Map<string, string>} */
 				const cacheKeyCache = new Map();
 				/**
+				 * Returns the key.
 				 * @param {string} url the url
 				 * @returns {string} the key
 				 */
@@ -496,6 +557,7 @@
 				};
 
 				/**
+				 * Returns the key.
 				 * @param {string} url the url
 				 * @returns {string} the key
 				 */
@@ -517,6 +579,7 @@
 
 				const getLockfile = cachedWithoutKey(
 					/**
+					 * Handles the callback logic for this hook.
 					 * @param {(err: Error | null, lockfile?: Lockfile) => void} callback callback
 					 * @returns {void}
 					 */
@@ -569,6 +632,7 @@
 				let lockfileUpdates;
 
 				/**
+				 * Stores the provided lockfile.
 				 * @param {Lockfile} lockfile lockfile instance
 				 * @param {string} url url to store
 				 * @param {LockfileEntry | "ignore" | "no-cache"} entry lockfile entry
@@ -608,6 +672,7 @@
 				};
 
 				/**
+				 * Stores the provided lockfile.
 				 * @param {Lockfile} lockfile lockfile
 				 * @param {string} url url
 				 * @param {ResolveContentResult} result result
@@ -637,12 +702,51 @@
 
 				for (const { scheme, fetch } of schemes) {
 					/**
+					 * Validate redirect location.
+					 * @param {string} location Location header value (relative or absolute)
+					 * @param {string} base current absolute URL
+					 * @returns {string} absolute, validated redirect target
+					 */
+					const validateRedirectLocation = (location, base) => {
+						/** @type {URL} */
+						let nextUrl;
+						try {
+							nextUrl = new URL(location, base);
+						} catch (err) {
+							throw new Error(
+								`Invalid redirect URL: ${sanitizeUrlForError(location)}`,
+								{ cause: err }
+							);
+						}
+						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;
+					};
+					/**
+					 * Processes the provided url.
 					 * @param {string} url URL
 					 * @param {string | null} integrity integrity
 					 * @param {(err: Error | null, resolveContentResult?: ResolveContentResult) => void} callback callback
+					 * @param {number=} redirectCount number of followed redirects
 					 */
-					const resolveContent = (url, integrity, callback) => {
+					const resolveContent = (
+						url,
+						integrity,
+						callback,
+						redirectCount = 0
+					) => {
 						/**
+						 * Processes the provided err.
 						 * @param {Error | null} err error
 						 * @param {FetchResult=} _result fetch result
 						 * @returns {void}
@@ -653,8 +757,19 @@
 							const result = /** @type {FetchResult} */ (_result);
 
 							if ("location" in result) {
+								// Validate redirect target before following
+								/** @type {string} */
+								let absolute;
+								try {
+									absolute = validateRedirectLocation(result.location, url);
+								} catch (err_) {
+									return callback(/** @type {Error} */ (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);
@@ -665,7 +780,8 @@
 											content,
 											storeLock: storeLock && result.storeLock
 										});
-									}
+									},
+									redirectCount + 1
 								);
 							}
 
@@ -689,6 +805,7 @@
 					};
 
 					/**
+					 * Processes the provided url.
 					 * @param {string} url URL
 					 * @param {FetchResult | RedirectFetchResult | undefined} cachedResult result from cache
 					 * @param {(err: Error | null, fetchResult?: FetchResult) => void} callback callback
@@ -715,6 +832,7 @@
 								requestTime
 							);
 							/**
+							 * Processes the provided partial result.
 							 * @param {Partial<Pick<FetchResultMeta, "fresh">> & (Pick<RedirectFetchResult, "location"> | Pick<ContentFetchResult, "content" | "entry">)} partialResult result
 							 * @returns {void}
 							 */
@@ -781,9 +899,17 @@
 								res.statusCode >= 301 &&
 								res.statusCode <= 308
 							) {
-								const result = {
-									location: new URL(location, url).href
-								};
+								/** @type {string} */
+								let absolute;
+								try {
+									absolute = validateRedirectLocation(location, url);
+								} catch (err) {
+									logger.log(
+										`GET ${url} [${res.statusCode}] -> ${String(location)} (rejected: ${/** @type {Error} */ (err).message})`
+									);
+									return callback(/** @type {Error} */ (err));
+								}
+								const result = { location: absolute };
 								if (
 									!cachedResult ||
 									!("location" in cachedResult) ||
@@ -820,9 +946,16 @@
 								stream = stream.pipe(createInflate());
 							}
 
-							stream.on("data", (chunk) => {
-								bufferArr.push(chunk);
-							});
+							stream.on(
+								"data",
+								/**
+								 * Handles the callback logic for this hook.
+								 * @param {Buffer} chunk chunk
+								 */
+								(chunk) => {
+									bufferArr.push(chunk);
+								}
+							);
 
 							stream.on("end", () => {
 								if (!res.complete) {
@@ -860,6 +993,7 @@
 
 					const fetchContent = cachedWithKey(
 						/**
+						 * Handles the callback logic for this hook.
 						 * @param {string} url URL
 						 * @param {(err: Error | null, result?: FetchResult) => void} callback callback
 						 * @returns {void}
@@ -878,16 +1012,35 @@
 					);
 
 					/**
+					 * Checks whether this http uri plugin is allowed.
 					 * @param {string} uri uri
 					 * @returns {boolean} true when allowed, otherwise false
 					 */
 					const isAllowed = (uri) => {
+						/** @type {URL} */
+						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;
+								/** @type {URL} */
+								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;
-							} else if (allowed.test(uri)) {
+								if (allowed(parsedUri.href)) return true;
+							} else if (allowed.test(parsedUri.href)) {
 								return true;
 							}
 						}
@@ -898,6 +1051,7 @@
 
 					const getInfo = cachedWithKey(
 						/**
+						 * Processes the provided url.
 						 * @param {string} url the url
 						 * @param {(err: Error | null, info?: Info) => void} callback callback
 						 * @returns {void}
@@ -970,6 +1124,7 @@
 								}
 								let entry = entryOrString;
 								/**
+								 * Processes the provided locked content.
 								 * @param {Buffer=} lockedContent locked content
 								 */
 								const doFetch = (lockedContent) => {
@@ -1050,6 +1205,7 @@
 										}
 										const content = /** @type {Buffer} */ (result);
 										/**
+										 * Continue with cached content.
 										 * @param {Buffer | undefined} _result result
 										 * @returns {void}
 										 */
@@ -1147,6 +1303,7 @@
 					);
 
 					/**
+					 * Respond with url module.
 					 * @param {URL} url url
 					 * @param {ResourceDataWithData} resourceData resource data
 					 * @param {(err: Error | null, result: true | void) => void} callback callback

--
Gitblit v1.9.3