Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security Fix for Path Traversal - huntr.dev #275

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
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