diff --git a/services/clsi/app.coffee b/services/clsi/app.coffee index fe9ad532fe..44d31575cc 100644 --- a/services/clsi/app.coffee +++ b/services/clsi/app.coffee @@ -50,26 +50,9 @@ staticServer = express.static Settings.path.compilesDir, setHeaders: (res, path, # that could be used in same-origin/XSS attacks. res.set("Content-Type", "text/plain") - - -app.get "/project/:project_id/output/*", (req, res, next) -> - basePath = Path.resolve("#{Settings.path.compilesDir}/#{req.params.project_id}") - path = Path.normalize("#{basePath}/#{req.params[0]}") - if path.slice(0, basePath.length) != basePath - logger.warn path: req.params[0], project_id: req.params.project_id, "trying to leave project directory, aborting" - res.send(404) - return - fs.lstat path, (error, stats) -> - if error? - if error.code == "ENOENT" - error.statusCode = 404 - return next(error) - if stats.isSymbolicLink() - error = new Error("file is a symlink") - error.statusCode = 404 - return next(error) - req.url = "/#{req.params.project_id}/#{req.params[0]}" - staticServer(req, res, next) +app.get "/project/:project_id/output/*", require("./app/js/SymlinkCheckerMiddlewear"), (req, res, next) -> + req.url = "/#{req.params.project_id}/#{req.params[0]}" + staticServer(req, res, next) app.get "/status", (req, res, next) -> res.send "CLSI is alive\n" diff --git a/services/clsi/app/coffee/SymlinkCheckerMiddlewear.coffee b/services/clsi/app/coffee/SymlinkCheckerMiddlewear.coffee new file mode 100644 index 0000000000..b7baf1fa77 --- /dev/null +++ b/services/clsi/app/coffee/SymlinkCheckerMiddlewear.coffee @@ -0,0 +1,17 @@ +Path = require("path") +fs = require("fs") +Settings = require("settings-sharelatex") +logger = require("logger-sharelatex") + + +module.exports = (req, res, next)-> + basePath = Path.resolve("#{Settings.path.compilesDir}/#{req.params.project_id}") + requestedFsPath = Path.normalize("#{basePath}/#{req.params[0]}") + fs.realpath requestedFsPath, (err, realFsPath)-> + if err? + return res.send(500) + else if requestedFsPath != realFsPath + logger.warn requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "trying to access a different file (symlink), aborting" + return res.send(404) + else + return next() diff --git a/services/clsi/test/unit/coffee/SymlinkCheckerMiddlewearTests.coffee b/services/clsi/test/unit/coffee/SymlinkCheckerMiddlewearTests.coffee new file mode 100644 index 0000000000..50bd4271b5 --- /dev/null +++ b/services/clsi/test/unit/coffee/SymlinkCheckerMiddlewearTests.coffee @@ -0,0 +1,60 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +assert = require('assert') +path = require('path') +sinon = require('sinon') +modulePath = path.join __dirname, "../../../app/js/SymlinkCheckerMiddlewear" +expect = require("chai").expect + +describe "SymlinkCheckerMiddlewear", -> + + beforeEach -> + + @settings = + path: + compilesDir: "/compiles/here" + + @fs = {} + @SymlinkCheckerMiddlewear = SandboxedModule.require modulePath, requires: + "settings-sharelatex":@settings + "logger-sharelatex": + log:-> + warn:-> + "fs":@fs + @req = + params: + project_id:"12345" + + @res = {} + @req.params[0]= "output.pdf" + + + describe "sending a normal file through", -> + beforeEach -> + @fs.realpath = sinon.stub().callsArgWith(1, null, "#{@settings.path.compilesDir}/#{@req.params.project_id}/output.pdf") + + it "should call next", (done)-> + @SymlinkCheckerMiddlewear @req, @res, done + + + describe "with a symlink file", -> + beforeEach -> + @fs.realpath = sinon.stub().callsArgWith(1, null, "/etc/#{@req.params.project_id}/output.pdf") + + it "should send a 404", (done)-> + @res.send = (resCode)-> + resCode.should.equal 404 + done() + @SymlinkCheckerMiddlewear @req, @res + + describe "with an error from fs.realpath", -> + + beforeEach -> + @fs.realpath = sinon.stub().callsArgWith(1, "error") + + it "should send a 500", (done)-> + @res.send = (resCode)-> + resCode.should.equal 500 + done() + @SymlinkCheckerMiddlewear @req, @res +