Skip to content
This repository has been archived by the owner on May 10, 2022. It is now read-only.

FS sandboxing #84

Merged
merged 6 commits into from
Oct 17, 2018
Merged

FS sandboxing #84

merged 6 commits into from
Oct 17, 2018

Conversation

andremedeiros
Copy link
Contributor

@andremedeiros andremedeiros commented Oct 9, 2018

Overview

TL;DR
This PR adds FS sandboxing to only allow our FS calls to run inside the DApp root or the OS' temporary directory.

@corpetty to review too.

Cool Spaceship Picture

Tesla Roadster

@andremedeiros andremedeiros requested a review from a team October 9, 2018 20:42
test/fs.js Outdated
done();
});

if(func === 'stat') return;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stat has some weirdness with the graceful-fs package for some reason, so I'm skipping it here.

Copy link
Contributor

@michaelsbradleyjr michaelsbradleyjr Oct 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I researched/experimented and found that to test stat, a callback must be used. Here's how I rewrote the relevant test:

it('should not throw exceptions on paths inside the temporary dir root', async () => {
  try {
    if (func === 'stat') {
      await new Promise((resolve, reject) => {
        fs[func](os.tmpdir() + '/foo', (err) => {
          if (err) {
            reject(err);
          } else {
            resolve();
          }
        });
      });
    } else {
      await fs[func](os.tmpdir() + '/foo');
    }
  } catch (e) {
    assert.doesNotThrow(() => { if (e) throw e; }, /EPERM/);
  }
});

You can verify it's working by changing /EPERM/ to /ENOENT/ — the tests for fs.stat and fs.statSync will fail. Note that the function passed to it() has become an async function. Trying to assert.doesNotThrow([func]) where [func] is an async function can't work, since async functions always return promises.

The callback problem has to do with our old version of fs-extra. With recent versions, fs.stat is fine to call without a callback, since all fs-extra functions return promises if no callback is supplied.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to update fs-extra instead?

Copy link
Contributor

@michaelsbradleyjr michaelsbradleyjr Oct 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a good thing to do, but it's seen several major releases since 2.1.2 (the version we're using currently); fs-extra 7.0.0 may require some additional changes to embark, but maybe not. In any case, seems like a good opportunity and justification for a major upgrade of that dependency.

if(!allInsideRestricted) break;
}

if(allInsideRestricted) return receiver.apply(binding, args);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we assume the function will always be binded to itself? So that we can get rid of a param in each call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't. Some aren't bound to self, like writeFile is bound to writeFileSync.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. There are a couple of cases like that, and I'm assuming there's a reason why.

const tmp = os.tmpdir();
let allInsideRestricted = true;

for(let i = 0; i < count; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the count, can't we just do args.length?
And if it's not a path arg, just continue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't know how many args are paths or not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could run path.resolve on the arg, and if it throws an error, we continue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I see what you're saying. I'm not sure this would work, because of the many different cases that we're trying to cover:

  • Some functions have path as the single argument we're validating, but some others have src, dest.
  • If we went through the args and, say, checked whether the path existed or not, we wouldn't be able to create new files or new directories
  • path.resolve is actually incredibly forgiving, as it doesn't validate to check what a valid path is, so for instance, things like these work:
'/Users/andremedeiros/src/github.com/embark-framework/embark/{"foo": "bar"}}'
> path.resolve('{"foo": "bar"}, \n{"omg": "noway"}')
'/Users/andremedeiros/src/github.com/embark-framework/embark/{"foo": "bar"}, \n{"omg": "noway"}'
> path.resolve('{"foo": "bar"}, \n{"omg": "noway"}', './')
'/Users/andremedeiros/src/github.com/embark-framework/embark/{"foo": "bar"}, \n{"omg": "noway"}'
> path.resolve('{"foo": "bar"}, \n{"omg": "noway"}', '../')
'/Users/andremedeiros/src/github.com/embark-framework/embark'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than passing a count, in case there's a situation where all the path args are not consecutive, what if you pass an array of positions. Assuming count is renamed to pos, then you could do:

for (const i of pos) {
...
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that would be more flexible, but in the case of optional parameters, it would still not work. Don't think we have any functions that have those now, but we need to keep it in mind just in case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. In such a case, pos would need to be computed against arguments with a helper function before passing it to restrictPath. The helper would be function-specific and need to apply the same rules as receiver does to figure out optional parameters.

@andremedeiros andremedeiros requested a review from corpetty October 9, 2018 21:00
function mkdirpSync() {
return fs.mkdirpSync.apply(fs.mkdirpSync, arguments);
return restrictPath(fs.mkdirpSync, fs.mkdirpSync, 1, arguments);
Copy link
Contributor

@michaelsbradleyjr michaelsbradleyjr Oct 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the binding for fs.mkdirpSync be fs? Same question re: the others.

Would it make more sense to pass a lamba receiver like

(...args) => fs.mkdirpSync(...args)

and then call it with receiver(...args)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes the binding is different then the caller :-(

Copy link
Contributor

@michaelsbradleyjr michaelsbradleyjr Oct 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then passing a lamba may be the simplest thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By lambda do you mean the actual call?

Copy link
Contributor

@michaelsbradleyjr michaelsbradleyjr Oct 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, an arrow function: e.g. (...args) => fs.mkdirpSync(...args).

If that arrow function is passed as the receiver argument, then you can call it with receiver(...args) inside restrictPath without needing .apply and a binding.

@alaibe
Copy link
Contributor

alaibe commented Oct 9, 2018

Have you tried to use: https://github.com/yahoo/fs-lock

@andremedeiros
Copy link
Contributor Author

I have not. That might have been a good idea. 👀

@michaelsbradleyjr
Copy link
Contributor

Re: fs-lock

YahooArchive/fs-lock#3 (comment)

@iurimatias
Copy link
Member

@andremedeiros please rebase

@iurimatias iurimatias merged commit ca9a958 into develop_51 Oct 17, 2018
@jrainville jrainville deleted the feature/blacklist-fs branch October 19, 2018 18:45
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants