From c857371fede8936a63be2f6d2cef7f3de496b1b6 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Fri, 26 Jun 2020 13:17:45 +0100 Subject: [PATCH] [misc] wordcount: restrict image to an allow list and add tests --- services/clsi/app/js/CompileController.js | 7 +++ .../test/acceptance/js/AllowedImageNames.js | 29 +++++++++++++ .../clsi/test/acceptance/js/helpers/Client.js | 9 ++++ .../test/unit/js/CompileControllerTests.js | 43 ++++++++++++++++++- 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/services/clsi/app/js/CompileController.js b/services/clsi/app/js/CompileController.js index c76d0d50e2..857d0504a3 100644 --- a/services/clsi/app/js/CompileController.js +++ b/services/clsi/app/js/CompileController.js @@ -218,6 +218,13 @@ module.exports = CompileController = { const { project_id } = req.params const { user_id } = req.params const { image } = req.query + if ( + image && + Settings.allowedImageNamesFlat && + Settings.allowedImageNamesFlat.indexOf(image) === -1 + ) { + return res.status(400).send('invalid image') + } logger.log({ image, file, project_id }, 'word count request') return CompileManager.wordcount(project_id, user_id, file, image, function( diff --git a/services/clsi/test/acceptance/js/AllowedImageNames.js b/services/clsi/test/acceptance/js/AllowedImageNames.js index 1ea0a36b64..a9b3996e1e 100644 --- a/services/clsi/test/acceptance/js/AllowedImageNames.js +++ b/services/clsi/test/acceptance/js/AllowedImageNames.js @@ -70,4 +70,33 @@ Hello world expect(pdf).to.not.exist }) }) + + describe('wordcount', function() { + beforeEach(function(done) { + Client.compile(this.project_id, this.request, done) + }) + it('should error out with an invalid imageName', function() { + Client.wordcountWithImage( + this.project_id, + 'main.tex', + 'something/evil:1337', + (error, result) => { + expect(String(error)).to.include('statusCode=400') + } + ) + }) + + it('should produce a texcout a valid imageName', function() { + Client.wordcountWithImage( + this.project_id, + 'main.tex', + process.env.TEXLIVE_IMAGE, + (error, result) => { + expect(error).to.not.exist + expect(result).to.exist + expect(result.texcount).to.exist + } + ) + }) + }) }) diff --git a/services/clsi/test/acceptance/js/helpers/Client.js b/services/clsi/test/acceptance/js/helpers/Client.js index d65941671a..c940e305f8 100644 --- a/services/clsi/test/acceptance/js/helpers/Client.js +++ b/services/clsi/test/acceptance/js/helpers/Client.js @@ -189,6 +189,11 @@ module.exports = Client = { }, wordcount(project_id, file, callback) { + const image = undefined + Client.wordcountWithImage(project_id, file, image, callback) + }, + + wordcountWithImage(project_id, file, image, callback) { if (callback == null) { callback = function(error, pdfPositions) {} } @@ -196,6 +201,7 @@ module.exports = Client = { { url: `${this.host}/project/${project_id}/wordcount`, qs: { + image, file } }, @@ -203,6 +209,9 @@ module.exports = Client = { if (error != null) { return callback(error) } + if (response.statusCode !== 200) { + return callback(new Error(`statusCode=${response.statusCode}`)) + } return callback(null, JSON.parse(body)) } ) diff --git a/services/clsi/test/unit/js/CompileControllerTests.js b/services/clsi/test/unit/js/CompileControllerTests.js index 4480c8804e..f3f3fa4ad8 100644 --- a/services/clsi/test/unit/js/CompileControllerTests.js +++ b/services/clsi/test/unit/js/CompileControllerTests.js @@ -12,6 +12,7 @@ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') require('chai').should() +const { expect } = require('chai') const modulePath = require('path').join( __dirname, '../../../app/js/CompileController' @@ -287,21 +288,59 @@ describe('CompileController', function() { this.CompileManager.wordcount = sinon .stub() .callsArgWith(4, null, (this.texcount = ['mock-texcount'])) - return this.CompileController.wordcount(this.req, this.res, this.next) }) it('should return the word count of a file', function() { + this.CompileController.wordcount(this.req, this.res, this.next) return this.CompileManager.wordcount .calledWith(this.project_id, undefined, this.file, this.image) .should.equal(true) }) - return it('should return the texcount info', function() { + it('should return the texcount info', function() { + this.CompileController.wordcount(this.req, this.res, this.next) return this.res.json .calledWith({ texcount: this.texcount }) .should.equal(true) }) + + describe('when allowedImageNamesFlat is set', function() { + beforeEach(function() { + this.Settings.allowedImageNamesFlat = [ + 'repo/image:tag1', + 'repo/image:tag2' + ] + this.res.send = sinon.stub() + this.res.status = sinon.stub().returns({ send: this.res.send }) + }) + + describe('with an invalid image', function() { + beforeEach(function() { + this.req.query.image = 'something/evil:1337' + this.CompileController.wordcount(this.req, this.res, this.next) + }) + it('should return a 400', function() { + expect(this.res.status.calledWith(400)).to.equal(true) + }) + it('should not run the query', function() { + expect(this.CompileManager.wordcount.called).to.equal(false) + }) + }) + + describe('with a valid image', function() { + beforeEach(function() { + this.req.query.image = 'repo/image:tag1' + this.CompileController.wordcount(this.req, this.res, this.next) + }) + it('should not return a 400', function() { + expect(this.res.status.calledWith(400)).to.equal(false) + }) + it('should run the query', function() { + expect(this.CompileManager.wordcount.called).to.equal(true) + }) + }) + }) }) })