CVE-2023-28155 (JavaScript)

The Request package through 2.88.1 for Node.js allows a bypass of SSRF mitigations via an attacker-controller server that does a cross-protocol redirect (HTTP to HTTPS, or HTTPS to HTTP). NOTE: This vulnerability only affects products that are no longer supported by the maintainer.

Weakness:
CWE-918: name: Server-Side Request Forgery (SSRF)
description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.

Repository: https://github.com/cypress-io/request

Vulnerable code:
file_path: lib/redirect.js
snippet:   function processRedirect (shouldRedirect) {
    if (!shouldRedirect) return callback(null, false)
    if (typeof shouldRedirect === 'string') {
      // overridden redirect url
      request.debug('redirect overridden', redirectTo)
      redirectTo = shouldRedirect
    }

    request.debug('redirect to', redirectTo)

    // ignore any potential response body.  it cannot possibly be useful
    // to us at this point.
    // response.resume should be defined, but check anyway before calling. Workaround for browserify.
    if (response.resume) {
      response.resume()
    }

    if (self.redirectsFollowed >= self.maxRedirects) {
      return callback(new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + request.uri.href))
    }
    self.redirectsFollowed += 1

    if (!isUrl.test(redirectTo)) {
      redirectTo = url.resolve(request.uri.href, redirectTo)
    }

    var uriPrev = request.uri
    request.uri = url.parse(redirectTo)

    // handle the case where we change protocol from https to http or vice versa
    if (request.uri.protocol !== uriPrev.protocol) {
      delete request.agent
    }

    self.redirects.push({ statusCode: response.statusCode, redirectUri: redirectTo })

    if (self.followAllRedirects && request.method !== 'HEAD' &&
      response.statusCode !== 401 && response.statusCode !== 307) {
      request.method = self.followOriginalHttpMethod ? request.method : 'GET'
    }
    // request.method = 'GET' // Force all redirects to use GET || commented out fixes #215
    delete request.src
    delete request.req
    delete request._started
    if (response.statusCode !== 401 && response.statusCode !== 307) {
      // Remove parameters from the previous response, unless this is the second request
      // for a server that requires digest authentication.
      delete request.body
      delete request._form
      if (request.headers) {
        request.removeHeader('host')
        request.removeHeader('content-type')
        request.removeHeader('content-length')
        if (request.uri.hostname !== request.originalHost.split(':')[0]) {
          // Remove authorization if changing hostnames (but not if just
          // changing ports or protocols).  This matches the behavior of curl:
          // https://github.com/bagder/curl/blob/6beb0eee/lib/http.c#L710
          request.removeHeader('authorization')
        }
      }
    }

    if (!self.removeRefererHeader) {
      request.setHeader('referer', uriPrev.href)
    }

    request.emit('redirect')
    request.init()
    callback(null, true)
  }

Fixed code:
file_path: lib/redirect.js
snippet:   function processRedirect (shouldRedirect) {
    if (!shouldRedirect) return callback(null, false)
    if (typeof shouldRedirect === 'string') {
      // overridden redirect url
      request.debug('redirect overridden', redirectTo)
      redirectTo = shouldRedirect
    }

    request.debug('redirect to', redirectTo)

    // ignore any potential response body.  it cannot possibly be useful
    // to us at this point.
    // response.resume should be defined, but check anyway before calling. Workaround for browserify.
    if (response.resume) {
      response.resume()
    }

    if (self.redirectsFollowed >= self.maxRedirects) {
      return callback(new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + request.uri.href))
    }
    self.redirectsFollowed += 1

    if (!isUrl.test(redirectTo)) {
      redirectTo = url.resolve(request.uri.href, redirectTo)
    }

    var uriPrev = request.uri
    request.uri = url.parse(redirectTo)

    // handle the case where we change protocol from https to http or vice versa
    if (request.uri.protocol !== uriPrev.protocol && self.allowInsecureRedirect) {
      delete request.agent
    }

    self.redirects.push({ statusCode: response.statusCode, redirectUri: redirectTo })

    if (self.followAllRedirects && request.method !== 'HEAD' &&
      response.statusCode !== 401 && response.statusCode !== 307) {
      request.method = self.followOriginalHttpMethod ? request.method : 'GET'
    }
    // request.method = 'GET' // Force all redirects to use GET || commented out fixes #215
    delete request.src
    delete request.req
    delete request._started
    if (response.statusCode !== 401 && response.statusCode !== 307) {
      // Remove parameters from the previous response, unless this is the second request
      // for a server that requires digest authentication.
      delete request.body
      delete request._form
      if (request.headers) {
        request.removeHeader('host')
        request.removeHeader('content-type')
        request.removeHeader('content-length')
        if (request.uri.hostname !== request.originalHost.split(':')[0]) {
          // Remove authorization if changing hostnames (but not if just
          // changing ports or protocols).  This matches the behavior of curl:
          // https://github.com/bagder/curl/blob/6beb0eee/lib/http.c#L710
          request.removeHeader('authorization')
        }
      }
    }

    if (!self.removeRefererHeader) {
      request.setHeader('referer', uriPrev.href)
    }

    request.emit('redirect')
    request.init()
    callback(null, true)
  }