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