diff --git a/README.md b/README.md index 9d543161..1982cb78 100755 --- a/README.md +++ b/README.md @@ -203,6 +203,7 @@ Instantiates a Connect server, setting up Superstatic middleware, port, host, de * `errorPage` - A file path to a custom error page. Defaults to [Superstatic's error page](https://github.com/firebase/superstatic/blob/master/lib/assets/not_found.html). * `debug` - A boolean value that tells Superstatic to show or hide network logging in the console. Defaults to `false`. * `compression` - A boolean value that tells Superstatic to serve gzip/deflate compressed responses based on the request Accept-Encoding header and the response Content-Type header. Defaults to `false`. + * `symlink` - A boolean value that tells Superstatic to resolve and show also `symlink` files. Defaults to `false` to prevent `path traversal attacks`. * `gzip` **[DEPRECATED]** - A boolean value which is now equivalent in behavior to `compression`. Defaults to `false`. ## Providers diff --git a/lib/cli/flags.js b/lib/cli/flags.js index dc18527f..7285f84c 100644 --- a/lib/cli/flags.js +++ b/lib/cli/flags.js @@ -31,6 +31,12 @@ module.exports = function(cli, options, ready) { done(); }); + cli.flag('--symlink') + .handler(function(shouldEnableSymlink, done) { + cli.set('symlink', shouldEnableSymlink); + done(); + }); + cli.flag('--gzip') .handler(function(shouldCompress, done) { cli.set('compression', shouldCompress); diff --git a/lib/cli/index.js b/lib/cli/index.js index 659f1608..36ff2ecc 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -16,6 +16,7 @@ var CONFIG_FILENAME = ['superstatic.json', 'firebase.json']; var ENV_FILENAME = '.env.json'; var DEBUG = false; var LIVE = false; +var SYMLINK = false; var env; try { @@ -35,6 +36,7 @@ module.exports = function() { cli.set('env', env); cli.set('debug', DEBUG); cli.set('live', LIVE); + cli.set('symlink', SYMLINK); // If no commands matched, the user probably // wants to run a server diff --git a/lib/cli/server.js b/lib/cli/server.js index 1d9cfb5c..ec31cd74 100644 --- a/lib/cli/server.js +++ b/lib/cli/server.js @@ -20,7 +20,7 @@ module.exports = function(cli, imports, ready) { var compression = cli.get('compression'); var env = cli.get('env'); var live = cli.get('live'); - + var symlink = cli.get('symlink'); var workingDirectory = data[0]; var options = { @@ -30,7 +30,8 @@ module.exports = function(cli, imports, ready) { compression: compression, debug: debug, env: env, - live: live + live: live, + symlink: symlink }; cli.set('options', options); diff --git a/lib/providers/fs.js b/lib/providers/fs.js index bb5d0aab..dda3c3d5 100644 --- a/lib/providers/fs.js +++ b/lib/providers/fs.js @@ -12,10 +12,11 @@ var pathjoin = require('join-path'); var RSVP = require('rsvp'); var _ = require('lodash'); -var statPromise = RSVP.denodeify(fs.stat); +var statPromise = RSVP.denodeify(fs.lstat); var multiStat = function(paths) { var pathname = paths.shift(); return statPromise(pathname).then(function(stat) { + stat.isSymb = stat.isSymbolicLink(); stat.path = pathname; return stat; }, function(err) { @@ -30,6 +31,8 @@ module.exports = function(options) { var etagCache = {}; var cwd = options.cwd || process.cwd(); var publicPaths = options.public || ['.']; + var symlink = options.symlink; + if (!_.isArray(publicPaths)) { publicPaths = [publicPaths]; } @@ -79,8 +82,18 @@ module.exports = function(options) { }); return multiStat(fullPathnames).then(function(stat) { - foundPath = stat.path; result.modified = stat.mtime.getTime(); + + if (!symlink) { + // Symlinks removed by default + if(stat.isSymb) { + return RSVP.reject({code: 'EINVAL'}); + } + + } + + foundPath = stat.path; + result.size = stat.size; return _fetchEtag(stat.path, stat); }).then(function(etag) { diff --git a/lib/responder.js b/lib/responder.js index 3f380c74..2e502a94 100644 --- a/lib/responder.js +++ b/lib/responder.js @@ -20,6 +20,7 @@ var Responder = function(req, res, options) { this.config = options.config || {}; this.rewriters = options.rewriters || {}; this.compressor = options.compressor; + this.symlink = options.symlink; }; Responder.prototype.isNotModified = function(stats) { diff --git a/lib/superstatic.js b/lib/superstatic.js index 326b7753..25f7d55a 100644 --- a/lib/superstatic.js +++ b/lib/superstatic.js @@ -42,7 +42,8 @@ var superstatic = function(spec) { // Set up provider var provider = spec.provider ? promiseback(spec.provider, 2) : fsProvider(_.extend({ - cwd: cwd // default current working directory + cwd: cwd, // default current working directory + symlink: spec.symlink // symlink }, config)); // Select compression middleware @@ -55,13 +56,15 @@ var superstatic = function(spec) { compressor = null; } + // Setup helpers router.use(function(req, res, next) { res.superstatic = new Responder(req, res, { provider: provider, config: config, compressor: compressor, - rewriters: spec.rewriters + rewriters: spec.rewriters, + symlink: spec.symlink }); next();