From 2d84b7d990f7e34a76d2d741d012c4fa5e35cd4f Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 20 Feb 2023 09:33:11 -0600 Subject: [PATCH] Upgraded got package from v9.6.0 to v11.8.6 (#16261) Refs TryGhost/Team#2459 -upgraded got from v9.6.0 to v11.8.6 to support following redirects (and other fixes) -got v12+ requires ESM, so we do not want to upgrade further at this time -required changes to a few libraries that use externalRequests -mention discovery service tests updated to test for follow redirects --- .../core/core/server/lib/request-external.js | 27 +++---- .../core/server/services/oembed/nft-oembed.js | 3 +- ghost/core/package.json | 2 +- ghost/core/test/e2e-api/admin/oembed.test.js | 34 ++++++++- .../unit/server/lib/request-external.test.js | 11 ++- ghost/members-api/lib/services/geolocation.js | 5 +- ghost/members-api/package.json | 2 +- ghost/oembed-service/lib/oembed-service.js | 63 +++------------- .../lib/MentionDiscoveryService.js | 2 +- .../webmentions/lib/MentionSendingService.js | 8 +- .../test/MentionDiscoveryService.test.js | 24 +++--- .../test/MentionSendingService.test.js | 75 +++++++++---------- 12 files changed, 117 insertions(+), 139 deletions(-) diff --git a/ghost/core/core/server/lib/request-external.js b/ghost/core/core/server/lib/request-external.js index 18fbbf6981..a311d2c3e1 100644 --- a/ghost/core/core/server/lib/request-external.js +++ b/ghost/core/core/server/lib/request-external.js @@ -20,22 +20,32 @@ function isPrivateIp(addr) { async function errorIfHostnameResolvesToPrivateIp(options) { // allow requests through to local Ghost instance const siteUrl = new URL(config.get('url')); - const requestUrl = new URL(options.href); + const requestUrl = new URL(options.url.href); if (requestUrl.host === siteUrl.host) { return Promise.resolve(); } - const result = await dnsPromises.lookup(options.hostname); + const result = await dnsPromises.lookup(options.url.hostname); if (isPrivateIp(result.address)) { return Promise.reject(new errors.InternalServerError({ message: 'URL resolves to a non-permitted private IP block', code: 'URL_PRIVATE_INVALID', - context: options.href + context: options.url.href })); } } +async function errorIfInvalidUrl(options) { + if (!options.url.hostname || !validator.isURL(options.url.hostname)) { + throw new errors.InternalServerError({ + message: 'URL invalid.', + code: 'URL_MISSING_INVALID', + context: options.url.href + }); + } +} + // same as our normal request lib but if any request in a redirect chain resolves // to a private IP address it will be blocked before the request is made. const externalRequest = got.extend({ @@ -44,16 +54,7 @@ const externalRequest = got.extend({ }, timeout: 10000, // default is no timeout hooks: { - init: [(options) => { - if (!options.hostname || !validator.isURL(options.hostname)) { - throw new errors.InternalServerError({ - message: 'URL empty or invalid.', - code: 'URL_MISSING_INVALID', - context: options.href - }); - } - }], - beforeRequest: [errorIfHostnameResolvesToPrivateIp], + beforeRequest: [errorIfInvalidUrl,errorIfHostnameResolvesToPrivateIp], beforeRedirect: [errorIfHostnameResolvesToPrivateIp] } }); diff --git a/ghost/core/core/server/services/oembed/nft-oembed.js b/ghost/core/core/server/services/oembed/nft-oembed.js index bc2a293e72..8f1ec570f5 100644 --- a/ghost/core/core/server/services/oembed/nft-oembed.js +++ b/ghost/core/core/server/services/oembed/nft-oembed.js @@ -42,9 +42,8 @@ class NFTOEmbedProvider { headers['X-API-KEY'] = this.dependencies.config.apiKey; } const result = await externalRequest(`https://api.opensea.io/api/v1/asset/${transaction}/${asset}/?format=json`, { - json: true, headers - }); + }).json(); return { version: '1.0', type: 'nft', diff --git a/ghost/core/package.json b/ghost/core/package.json index 331de0e697..afd590dd3a 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -170,7 +170,7 @@ "fs-extra": "11.1.0", "ghost-storage-base": "1.0.0", "glob": "8.1.0", - "got": "9.6.0", + "got": "11.8.6", "gscan": "4.36.0", "human-number": "2.0.1", "image-size": "1.0.2", diff --git a/ghost/core/test/e2e-api/admin/oembed.test.js b/ghost/core/test/e2e-api/admin/oembed.test.js index 54db7dacaf..6f0f2fca48 100644 --- a/ghost/core/test/e2e-api/admin/oembed.test.js +++ b/ghost/core/test/e2e-api/admin/oembed.test.js @@ -128,6 +128,12 @@ describe('Oembed API', function () { }); it('errors when fetched url is an IP address', async function () { + // in order to follow the 302, we need to stub differently; externalRequest will block the internal IP + dnsPromises.lookup.restore(); + let dnsStub = sinon.stub(dnsPromises, 'lookup'); + dnsStub.onCall(0).returns(Promise.resolve({address: '123.123.123.123'})); + dnsStub.onCall(1).returns(Promise.resolve({address: '0.0.0.0'})); + const redirectMock = nock('http://test.com/') .get('/') .reply(302, undefined, {Location: 'http://0.0.0.0:8080'}); @@ -147,7 +153,7 @@ describe('Oembed API', function () { .expect('Cache-Control', testUtils.cacheRules.private) .expect(422); - pageMock.isDone().should.be.true(); + pageMock.isDone().should.be.false(); // we shouldn't hit this; blocked by externalRequest should.exist(res.body.errors); }); @@ -376,6 +382,15 @@ describe('Oembed API', function () { }); it('skips fetching IPv4 addresses', async function () { + dnsPromises.lookup.restore(); + sinon.stub(dnsPromises, 'lookup').callsFake(function (hostname) { + if (hostname === '192.168.0.1') { + return Promise.resolve({address: '192.168.0.1'}); + } else { + return Promise.resolve({address: '123.123.123.123'}); + } + }); + const pageMock = nock('http://test.com') .get('/') .reply(200, '
'); @@ -399,6 +414,15 @@ describe('Oembed API', function () { }); it('skips fetching IPv6 addresses', async function () { + dnsPromises.lookup.restore(); + sinon.stub(dnsPromises, 'lookup').callsFake(function (hostname) { + if (hostname === '[2607:f0d0:1002:51::4]') { + return Promise.resolve({address: '192.168.0.1'}); + } else { + return Promise.resolve({address: '123.123.123.123'}); + } + }); + const pageMock = nock('http://test.com') .get('/') .reply(200, ''); @@ -422,6 +446,14 @@ describe('Oembed API', function () { }); it('skips fetching localhost', async function () { + dnsPromises.lookup.restore(); + sinon.stub(dnsPromises, 'lookup').callsFake(function (hostname) { + if (hostname === 'localhost') { + return Promise.resolve({address: '127.0.0.1'}); + } else { + return Promise.resolve({address: '123.123.123.123'}); + } + }); const pageMock = nock('http://test.com') .get('/') .reply(200, ''); diff --git a/ghost/core/test/unit/server/lib/request-external.test.js b/ghost/core/test/unit/server/lib/request-external.test.js index 8ad9818108..62a67e9861 100644 --- a/ghost/core/test/unit/server/lib/request-external.test.js +++ b/ghost/core/test/unit/server/lib/request-external.test.js @@ -263,7 +263,7 @@ describe('External Request', function () { throw new Error('Request should have rejected with invalid url message'); }, (err) => { should.exist(err); - err.message.should.be.equal('URL empty or invalid.'); + err.code.should.be.equal('ERR_INVALID_URL'); }); }); @@ -279,7 +279,8 @@ describe('External Request', function () { throw new Error('Request should have rejected with invalid url message'); }, (err) => { should.exist(err); - err.message.should.be.equal('URL empty or invalid.'); + // got v11+ throws an error instead of the external requests lib + err.message.should.be.equal('No URL protocol specified'); }); }); @@ -300,7 +301,7 @@ describe('External Request', function () { }, (err) => { requestMock.isDone().should.be.true(); should.exist(err); - err.statusMessage.should.be.equal('Not Found'); + err.response.statusMessage.should.be.equal('Not Found'); }); }); @@ -322,9 +323,7 @@ describe('External Request', function () { }, (err) => { requestMock.isDone().should.be.true(); should.exist(err); - err.statusMessage.should.be.equal('Internal Server Error'); - err.body.should.match(/something awful happened/); - err.body.should.match(/AWFUL_ERROR/); + err.response.statusMessage.should.be.equal(`Internal Server Error`); }); }); }); diff --git a/ghost/members-api/lib/services/geolocation.js b/ghost/members-api/lib/services/geolocation.js index 0f745c7ed3..72f7e907c7 100644 --- a/ghost/members-api/lib/services/geolocation.js +++ b/ghost/members-api/lib/services/geolocation.js @@ -7,9 +7,8 @@ module.exports = class GeolocationService { if (!ipAddress || (!IPV4_REGEX.test(ipAddress) && !IPV6_REGEX.test(ipAddress))) { return; } - const geojsUrl = `https://get.geojs.io/v1/ip/geo/${encodeURIComponent(ipAddress)}.json`; - const response = await got(geojsUrl, {json: true, timeout: 500}); - return response.body; + const response = await got(geojsUrl, {timeout: 500}).json(); + return response; } }; diff --git a/ghost/members-api/package.json b/ghost/members-api/package.json index 6ea79044c6..cd3e500cc6 100644 --- a/ghost/members-api/package.json +++ b/ghost/members-api/package.json @@ -41,7 +41,7 @@ "body-parser": "1.20.1", "bson-objectid": "2.0.4", "express": "4.18.2", - "got": "9.6.0", + "got": "11.8.6", "jsonwebtoken": "8.5.1", "lodash": "4.17.21", "moment": "2.29.4", diff --git a/ghost/oembed-service/lib/oembed-service.js b/ghost/oembed-service/lib/oembed-service.js index 1bd7dbd7fe..373592cc0b 100644 --- a/ghost/oembed-service/lib/oembed-service.js +++ b/ghost/oembed-service/lib/oembed-service.js @@ -4,7 +4,6 @@ const logging = require('@tryghost/logging'); const {extract, hasProvider} = require('oembed-parser'); const cheerio = require('cheerio'); const _ = require('lodash'); -const {CookieJar} = require('tough-cookie'); const charset = require('charset'); const iconv = require('iconv-lite'); @@ -68,16 +67,7 @@ class OEmbed { this.config = config; /** @type {IExternalRequest} */ - this.externalRequest = async (url, requestConfig) => { - if (this.isIpOrLocalhost(url)) { - return this.unknownProvider(url); - } - const response = await externalRequest(url, requestConfig); - if (this.isIpOrLocalhost(response.url)) { - return this.unknownProvider(url); - } - return response; - }; + this.externalRequest = externalRequest; /** @type {ICustomProvider[]} */ this.customProviders = []; @@ -117,16 +107,13 @@ class OEmbed { * @param {string} url * @param {Object} options * - * @returns {Promise<{url: string, body: any, headers: any}>} + * @returns {GotPromise