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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'followsymlinks' option which allows rejecting symlinks #127

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

jayk
Copy link
Contributor

@jayk jayk commented Oct 10, 2019

Adds new option 'followssymlinks' which, when set to false, will reject any paths that contain symlinks.

Is smart enough to ignore symlinks below the root directory, and only rejects symlinks below the root.

Default behavior is completely unchanged. Tests included.

Resolves #120

@gregmartyn
Copy link

I think this has a symlink race unless we manage to pass O_NOFOLLOW to fs.open. That wouldn't let realroot have symlinks though (unless there's a way to call openat?) and I don't know how to pass our intention to send.

@jayk
Copy link
Contributor Author

jayk commented Oct 21, 2019

Hi Greg,

I'm not sure what you mean. I see that it is theoretically possible to serve a file that we should disallow, if a request has determined that there is no symlink, and in the interim the filesystem is changed to include a symlink... but this would have to happen in the very small period of time between when node returns from line 109 and when it executes line 126 and would only result in a file served if the path existed as a valid path (without symlinks) in the first place, in which case, we would have been serving it prior anyway.

Is that what you are referring to? If you can explain your concern I'll attempt to mitigate it.

Jay

@gregmartyn
Copy link

Right -- classic TOCTOU. Further protecting us is that the check you're doing is synchronous, so you'd have to be running something like pm2 or kubernetes to be vulnerable.

That said, followsymlinks is intended to protect against an attacker that has the ability to create symlinks, (otherwise there wouldn't be a vulnerable symlink in the first place) and our check (this PR) is running in response to a request that the attacker can generate, (the HTTP request for the static content) so the attacker is control of the timing of both the creation of the symlink and the validation of it.

It looks like that var stream = send(req, path, opts) is ultimately doing fs.createReadStream(path, options), (https://github.com/pillarjs/send/blob/master/index.js#L796) so if we set opts.flags = O_RDONLY & O_NOFOLLOW, we'd get the protection we seek. I don't see a openat (http://man7.org/linux/man-pages/man2/open.2.html) equivalent for createReadStream so we wouldn't be able to have a symlink in the realroot.

Let me know if that works, or if you'd like me to test something out.

Thanks for working on this PR!

@jayk
Copy link
Contributor Author

jayk commented Oct 22, 2019

I've updated the patch to pass flags O_NOFOLLOW and O_RDONLY when followsymlinks is false. I also changed it so that if you choose 'followsymlinks=false' then it will resolve the root path to the real path at startup, which should still allow symlinks in the root path, at the cost of locking the root path at startup time. With followsymlinks=true (the default) the behavior is unchanged from prior to the patch.

@dougwilson dougwilson added the pr label Feb 2, 2020
@@ -759,6 +764,89 @@ describe('serveStatic()', function () {
.get('/todo/')
.expect(404, done)
})
});

(skipSymlinks ? describe.skip : describe)('symlink tests', function () {
Copy link

Choose a reason for hiding this comment

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

(skipSymlinks ? describe.skip : describe)

Clever! Never seen this done before but makes sense :)

@gregmartyn
Copy link

Is anyone able to merge this? @dougwilson?

if (followsymlinks === false) {
fullpath = realroot + path
try {
realpath = fs.realpathSync(realroot + path)
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is happening in the even loop of a request, we should probably use the async of this, not sync so it doesn't block unnecessarily

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Disable symlinks
4 participants