Skip to content
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions lib/cli/flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions lib/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/cli/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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);
Expand Down
37 changes: 34 additions & 3 deletions lib/providers/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -26,10 +27,26 @@ var multiStat = function(paths) {
});
};

var symlinkPath = function(requestedpath, fullpath) {
var dirs = requestedpath.split('/');
if( dirs.length < 2) return false;
var basepath = fullpath.replace(new RegExp(requestedpath+"$"),"");
var testpath = basepath;
for( var i = 1; i < dirs.length-1; i++ ){
testpath = pathjoin(testpath, dirs[i]);
var stat = fs.lstatSync(testpath);
if( stat.isSymbolicLink() ){
return true;
}
}
};

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];
}
Expand Down Expand Up @@ -79,9 +96,23 @@ module.exports = function(options) {
});

return multiStat(fullPathnames).then(function(stat) {
foundPath = stat.path;
result.modified = stat.mtime.getTime();
result.size = stat.size;

if (!symlink) {
// Symlinks removed by default
if(stat.isSymb) {
return RSVP.reject({code: 'EINVAL'});
}
// If file is not symlink verify its not accessed by symlinked directory
if(symlinkPath(pathname, stat.path)) {
return RSVP.reject({code: 'EINVAL'});
}

}

foundPath = stat.path;

result.size = fs.statSync(foundPath).size;
return _fetchEtag(stat.path, stat);
}).then(function(etag) {
result.etag = etag;
Expand Down
1 change: 1 addition & 0 deletions lib/responder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 5 additions & 2 deletions lib/superstatic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down