From e1bee3c64715314ca2b509b7ffcd4da75bee07c1 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Wed, 3 Aug 2022 15:59:08 +0200 Subject: [PATCH] Implemented admin auth origin check (#15135) refs https://github.com/TryGhost/Team/issues/1694 - Added replacements option to `@tryghost/minifier` + updated documentation and name of 'options' param which was a bit confusing. - At compile time, we'll replace `'{{SITE_ORIGIN}}'` with the actual and JS encoded origin string. - Block requests to the auth frame with the wrong origin, but log a warning for now to make debugging easier. - Limit who can read the response messages by origin --- .../services/admin-auth-assets/service.js | 22 ++++++++++++-- .../src/admin-auth/message-handler.js | 10 +++++-- ghost/minifier/lib/minifier.js | 30 +++++++++++++++---- ghost/minifier/test/minifier.test.js | 15 ++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/ghost/core/core/frontend/services/admin-auth-assets/service.js b/ghost/core/core/frontend/services/admin-auth-assets/service.js index 63b391df1d..080e39f7c8 100644 --- a/ghost/core/core/frontend/services/admin-auth-assets/service.js +++ b/ghost/core/core/frontend/services/admin-auth-assets/service.js @@ -4,6 +4,7 @@ const path = require('path'); const fs = require('fs').promises; const logging = require('@tryghost/logging'); const config = require('../../../shared/config'); +const urlUtils = require('../../../shared/url-utils'); class AdminAuthAssetsService { constructor(options = {}) { @@ -24,13 +25,27 @@ class AdminAuthAssetsService { }; } + /** + * @private + */ + generateReplacements() { + // Clean the URL, only keep schema, host and port (without trailing slashes or subdirectory) + const url = new URL(urlUtils.getSiteUrl()); + const origin = url.origin; + + return { + // Properly encode the origin + '\'{{SITE_ORIGIN}}\'': JSON.stringify(origin) + }; + } + /** * @private * @returns {Promise} */ - async minify(globs) { + async minify(globs, options) { try { - await this.minifier.minify(globs); + await this.minifier.minify(globs, options); } catch (error) { if (error.code === 'EACCES') { logging.error('Ghost was not able to write admin-auth asset files due to permissions.'); @@ -84,8 +99,9 @@ class AdminAuthAssetsService { */ async load() { const globs = this.generateGlobs(); + const replacements = this.generateReplacements(); await this.clearFiles(); - await this.minify(globs); + await this.minify(globs, {replacements}); await this.copyStatic(); } } diff --git a/ghost/core/core/frontend/src/admin-auth/message-handler.js b/ghost/core/core/frontend/src/admin-auth/message-handler.js index 191620fa6b..465397dd0c 100644 --- a/ghost/core/core/frontend/src/admin-auth/message-handler.js +++ b/ghost/core/core/frontend/src/admin-auth/message-handler.js @@ -1,8 +1,12 @@ const adminUrl = window.location.href.replace('auth-frame/', ''); +// At compile time, we'll replace the value with the actual origin. +const siteOrigin = '{{SITE_ORIGIN}}'; + window.addEventListener('message', async function (event) { - if (event.origin !== '*') { - // return; + if (event.origin !== siteOrigin) { + console.warn('Ignored message to admin auth iframe because of mismatch in origin', 'expected', siteOrigin, 'got', event.origin, 'with data', event.data); + return; } let data = null; try { @@ -16,7 +20,7 @@ window.addEventListener('message', async function (event) { uid: data.uid, error: error, result: result - }), '*'); + }), siteOrigin); } if (data.action === 'getUser') { diff --git a/ghost/minifier/lib/minifier.js b/ghost/minifier/lib/minifier.js index 5d90d59810..c1c2ad4b36 100644 --- a/ghost/minifier/lib/minifier.js +++ b/ghost/minifier/lib/minifier.js @@ -109,14 +109,34 @@ class Minifier { } } - async minify(options) { - debug('Begin', options); - const destinations = Object.keys(options); + /** + * Minify files + * + * @param {Object} globs An object in the form of + * ```js + * { + * 'destination1.js': 'glob/*.js', + * 'destination2.js': 'glob2/*.js' + * } + * ``` + * @param {Object} [options] + * @param {Object} [options.replacements] Key value pairs that should get replaced in the content before minifying + * @returns {Promise} List of minified files (keys of globs) + */ + async minify(globs, options) { + debug('Begin', globs); + const destinations = Object.keys(globs); const minifiedFiles = []; for (const dest of destinations) { - const src = options[dest]; - const contents = await this.getSrcFileContents(src); + const src = globs[dest]; + let contents = await this.getSrcFileContents(src); + + if (options?.replacements) { + for (const key of Object.keys(options.replacements)) { + contents = contents.replace(key, options.replacements[key]); + } + } let minifiedContents; if (dest.endsWith('.css')) { diff --git a/ghost/minifier/test/minifier.test.js b/ghost/minifier/test/minifier.test.js index a25e6bae57..2c27558774 100644 --- a/ghost/minifier/test/minifier.test.js +++ b/ghost/minifier/test/minifier.test.js @@ -71,6 +71,21 @@ describe('Minifier', function () { result.should.be.an.Array().with.lengthOf(2); }); + + it('can replace the content', async function () { + let result = await minifier.minify({ + 'card.min.js': 'js/*.js' + }, { + replacements: { + '.kg-gallery-image': 'randomword' + } + }); + result.should.be.an.Array().with.lengthOf(1); + + const outputPath = minifier.getFullDest(result[0]); + const content = await fs.readFile(outputPath, {encoding: 'utf8'}); + content.should.match(/randomword/); + }); }); describe('Bad inputs', function () {