@@ -34,6 +34,8 @@ const memoize = require("../util/memoize");
3434const getHttp = memoize ( ( ) => require ( "http" ) ) ;
3535const getHttps = memoize ( ( ) => require ( "https" ) ) ;
3636
37+ const MAX_REDIRECTS = 5 ;
38+
3739/**
3840 * @param {typeof import("http") | typeof import("https") } request request
3941 * @param {string | URL | undefined } proxy proxy
@@ -200,6 +202,22 @@ const areLockfileEntriesEqual = (a, b) =>
200202const entryToString = ( entry ) =>
201203 `resolved: ${ entry . resolved } , integrity: ${ entry . integrity } , contentType: ${ entry . contentType } ` ;
202204
205+ /**
206+ * Sanitize URL for inclusion in error messages
207+ * @param {string } href URL string to sanitize
208+ * @returns {string } sanitized URL text for logs/errors
209+ */
210+ const sanitizeUrlForError = ( href ) => {
211+ try {
212+ const u = new URL ( href ) ;
213+ return `${ u . protocol } //${ u . host } ` ;
214+ } catch ( _err ) {
215+ return String ( href )
216+ . slice ( 0 , 200 )
217+ . replace ( / [ \r \n ] / g, "" ) ;
218+ }
219+ } ;
220+
203221class Lockfile {
204222 constructor ( ) {
205223 this . version = 1 ;
@@ -636,12 +654,46 @@ class HttpUriPlugin {
636654 } ;
637655
638656 for ( const { scheme, fetch } of schemes ) {
657+ /**
658+ * @param {string } location Location header value (relative or absolute)
659+ * @param {string } base current absolute URL
660+ * @returns {string } absolute, validated redirect target
661+ */
662+ const validateRedirectLocation = ( location , base ) => {
663+ let nextUrl ;
664+ try {
665+ nextUrl = new URL ( location , base ) ;
666+ } catch ( _err ) {
667+ throw new Error (
668+ `Invalid redirect URL: ${ sanitizeUrlForError ( location ) } `
669+ ) ;
670+ }
671+ if ( nextUrl . protocol !== "http:" && nextUrl . protocol !== "https:" ) {
672+ throw new Error (
673+ `Redirected URL uses disallowed protocol: ${ sanitizeUrlForError ( nextUrl . href ) } `
674+ ) ;
675+ }
676+ if ( ! isAllowed ( nextUrl . href ) ) {
677+ throw new Error (
678+ `${ nextUrl . href } doesn't match the allowedUris policy after redirect. These URIs are allowed:\n${ allowedUris
679+ . map ( ( uri ) => ` - ${ uri } ` )
680+ . join ( "\n" ) } `
681+ ) ;
682+ }
683+ return nextUrl . href ;
684+ } ;
639685 /**
640686 * @param {string } url URL
641687 * @param {string | null } integrity integrity
642688 * @param {(err: Error | null, resolveContentResult?: ResolveContentResult) => void } callback callback
689+ * @param {number= } redirectCount number of followed redirects
643690 */
644- const resolveContent = ( url , integrity , callback ) => {
691+ const resolveContent = (
692+ url ,
693+ integrity ,
694+ callback ,
695+ redirectCount = 0
696+ ) => {
645697 /**
646698 * @param {Error | null } err error
647699 * @param {FetchResult= } _result fetch result
@@ -653,8 +705,18 @@ class HttpUriPlugin {
653705 const result = /** @type {FetchResult } */ ( _result ) ;
654706
655707 if ( "location" in result ) {
708+ // Validate redirect target before following
709+ let absolute ;
710+ try {
711+ absolute = validateRedirectLocation ( result . location , url ) ;
712+ } catch ( err_ ) {
713+ return callback ( /** @type {Error } */ ( err_ ) ) ;
714+ }
715+ if ( redirectCount >= MAX_REDIRECTS ) {
716+ return callback ( new Error ( "Too many redirects" ) ) ;
717+ }
656718 return resolveContent (
657- result . location ,
719+ absolute ,
658720 integrity ,
659721 ( err , innerResult ) => {
660722 if ( err ) return callback ( err ) ;
@@ -665,7 +727,8 @@ class HttpUriPlugin {
665727 content,
666728 storeLock : storeLock && result . storeLock
667729 } ) ;
668- }
730+ } ,
731+ redirectCount + 1
669732 ) ;
670733 }
671734
@@ -781,9 +844,16 @@ class HttpUriPlugin {
781844 res . statusCode >= 301 &&
782845 res . statusCode <= 308
783846 ) {
784- const result = {
785- location : new URL ( location , url ) . href
786- } ;
847+ let absolute ;
848+ try {
849+ absolute = validateRedirectLocation ( location , url ) ;
850+ } catch ( err ) {
851+ logger . log (
852+ `GET ${ url } [${ res . statusCode } ] -> ${ String ( location ) } (rejected: ${ /** @type {Error } */ ( err ) . message } )`
853+ ) ;
854+ return callback ( /** @type {Error } */ ( err ) ) ;
855+ }
856+ const result = { location : absolute } ;
787857 if (
788858 ! cachedResult ||
789859 ! ( "location" in cachedResult ) ||
0 commit comments