diff --git a/server/lib/gateway/gateway.downloadBackup.js b/server/lib/gateway/gateway.downloadBackup.js index 99b2cb0f74..657f79408f 100644 --- a/server/lib/gateway/gateway.downloadBackup.js +++ b/server/lib/gateway/gateway.downloadBackup.js @@ -1,6 +1,7 @@ const path = require('path'); const fse = require('fs-extra'); const fs = require('fs'); +const { isURL, validateUrl } = require('../../utils/url'); const logger = require('../../utils/logger'); const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../utils/constants'); const { exec } = require('../../utils/childProcess'); @@ -20,8 +21,9 @@ async function downloadBackup(fileUrl) { if (encryptKey === null) { throw new NotFoundError('GLADYS_GATEWAY_BACKUP_KEY_NOT_FOUND'); } - // Extract file name - const fileWithoutSignedParams = fileUrl.split('?')[0]; + + // validate url + const fileWithoutSignedParams = isURL(fileUrl) ? validateUrl(fileUrl) : fileUrl; const restoreFolderPath = path.join(this.config.backupsFolder, RESTORE_FOLDER); // we ensure the restore backup folder exists await fse.ensureDir(restoreFolderPath); diff --git a/server/test/lib/gateway/gateway.downloadBackup.test.js b/server/test/lib/gateway/gateway.downloadBackup.test.js index 23901d3838..f6f918d3b2 100644 --- a/server/test/lib/gateway/gateway.downloadBackup.test.js +++ b/server/test/lib/gateway/gateway.downloadBackup.test.js @@ -7,7 +7,7 @@ const GladysGatewayClientMock = require('./GladysGatewayClientMock.test'); const getConfig = require('../../../utils/getConfig'); const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants'); -const { NotFoundError } = require('../../../utils/coreErrors'); +const { NotFoundError, InvalidURL } = require('../../../utils/coreErrors'); const { fake, assert } = sinon; const Gateway = proxyquire('../../../lib/gateway', { @@ -94,4 +94,18 @@ describe('gateway.downloadBackup', () => { }, }); }); + + it('should throw an error for invalid backup url', async () => { + const backupUrl = 'https://test.example/path/test.enc#&?`id`'; + try { + await gateway.downloadBackup(backupUrl); + assert.fail(); + } catch (e) { + expect(e) + .instanceOf(InvalidURL) + .haveOwnProperty('message', 'INVALID_URL'); + } + + assert.notCalled(event.emit); + }); }); diff --git a/server/test/utils/url.test.js b/server/test/utils/url.test.js new file mode 100644 index 0000000000..f9cad748e7 --- /dev/null +++ b/server/test/utils/url.test.js @@ -0,0 +1,26 @@ +const { expect } = require('chai'); +const { isURL, validateUrl } = require('../../utils/url'); + +describe('url.validation', () => { + it('should return true for a valid url', () => { + const url = 'https://example.com'; + expect(isURL(url)).to.equal(true); + }); + + it('should return false for an invalid url', () => { + const url = '/a/b'; + expect(isURL(url)).to.equal(false); + }); + + it('should return valid url', () => { + const url = 'https://example.com/test'; + expect(validateUrl(url)).to.be.equal('https://example.com/test'); + }); + + it('should throw an error for malicious url', () => { + const url = 'https://example.com/test?#a'; + expect(() => { + validateUrl(url); + }).to.throw(Error); + }); +}); diff --git a/server/utils/coreErrors.js b/server/utils/coreErrors.js index 74b3c3ff73..d65370f2f0 100644 --- a/server/utils/coreErrors.js +++ b/server/utils/coreErrors.js @@ -12,6 +12,12 @@ class NotFoundError extends Error { } } +class InvalidURL extends Error { + constructor(message) { + super(); + this.message = message; + } +} class NoValuesFoundError extends Error { constructor(message) { super(); @@ -80,4 +86,5 @@ module.exports = { ConflictError, ForbiddenError, TooManyRequests, + InvalidURL, }; diff --git a/server/utils/url.js b/server/utils/url.js new file mode 100644 index 0000000000..6550281635 --- /dev/null +++ b/server/utils/url.js @@ -0,0 +1,42 @@ +const Joi = require('joi'); +const { InvalidURL } = require('./coreErrors'); + +/** + * @description Typeof url. + * @param {string} str - The url of the backup. + * @returns {boolean} Return true for valid url. + * @example + * isURL(); + */ +const isURL = (str) => { + try { + const url = new URL(str); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch (_) { + return false; + } +}; + +/** + * @description Validate the url. + * @param {string} url - The url of the backup. + * @returns {string} Return a valid url. + * @example + * validateUrl(); + */ +function validateUrl(url) { + const schema = Joi.string() + .uri() + .pattern(/^[^?#]*$/, ''); + const { error, value } = schema.validate(url); + if (error) { + throw new InvalidURL('INVALID_URL'); + } else { + return value; + } +} + +module.exports = { + isURL, + validateUrl, +};