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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ all methods.

The default value is `true`.

##### followsymlinks

Determines how serve-static will handle how files or paths containing symlinks
are handled. Setting `followsymlinks` to `false` will cause serve-static to
reject requests for files that have a symlink in their path.

The default value is `true`.

Note that setting `followsymlinks` to `false` also causes the the module
to resolve any symbolic links in the root path during startup. This means
that if your root path does contain symlinks, changes to those symlinks after
application startup will not be noticed.


##### immutable

Enable or disable the `immutable` directive in the `Cache-Control` response
Expand Down
39 changes: 39 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ var encodeUrl = require('encodeurl')
var escapeHtml = require('escape-html')
var parseUrl = require('parseurl')
var resolve = require('path').resolve
var fs = require('fs')
var constants = fs.constants || require('constants') // eslint-disable-line node/no-deprecated-api
var send = require('send')
var url = require('url')

Expand Down Expand Up @@ -50,6 +52,10 @@ function serveStatic (root, options) {
// fall-though
var fallthrough = opts.fallthrough !== false

// handle symlinks
var realroot
var followsymlinks = true

// default redirect
var redirect = opts.redirect !== false

Expand All @@ -64,6 +70,16 @@ function serveStatic (root, options) {
opts.maxage = opts.maxage || opts.maxAge || 0
opts.root = resolve(root)

// only set followsymlinks to false if it was explicitly set
if (opts.followsymlinks === false) {
followsymlinks = false
realroot = fs.realpathSync(root)
opts.flags = constants.O_RDONLY | constants.O_NOFOLLOW
// if followsymlinks is disabled, we need the fully resolved
// (un-symlink'd) root to start
opts.root = realroot
}

// construct directory listener
var onDirectory = redirect
? createRedirectDirectoryListener()
Expand All @@ -86,12 +102,35 @@ function serveStatic (root, options) {
var forwardError = !fallthrough
var originalUrl = parseUrl.original(req)
var path = parseUrl(req).pathname
var fullpath, realpath

// make sure redirect occurs at mount
if (path === '/' && originalUrl.pathname.substr(-1) !== '/') {
path = ''
}

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

} catch (e) {
realpath = undefined
}
// if the full path and the real path are not the same,
// then there is a symlink somewhere along the way
if (fullpath !== realpath) {
if (fallthrough) {
return next()
}

// forbidden on symlinks
res.statusCode = 403
res.setHeader('Content-Length', '0')
res.end()
return
}
}

// create send stream
var stream = send(req, path, opts)

Expand Down
1 change: 1 addition & 0 deletions test/fixtures/members
1 change: 1 addition & 0 deletions test/fixtures/symroot
1 change: 1 addition & 0 deletions test/fixtures/users/william.txt
88 changes: 88 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ var assert = require('assert')
var Buffer = require('safe-buffer').Buffer
var http = require('http')
var path = require('path')
var fs = require('fs')
var request = require('supertest')
var serveStatic = require('..')

var fixtures = path.join(__dirname, '/fixtures')
var relative = path.relative(process.cwd(), fixtures)

var skipRelative = ~relative.indexOf('..') || path.resolve(relative) === relative
var skipSymlinks = true
try {
skipSymlinks = fs.realpathSync(fixtures + '/users/tobi.txt') !== fs.realpathSync(fixtures + '/members/tobi.txt')
} catch (e) {}

describe('serveStatic()', function () {
describe('basic operations', function () {
Expand Down Expand Up @@ -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 :)

describe('when followsymlinks is false', function () {
var server
before(function () {
server = createServer(fixtures, { followsymlinks: false, fallthrough: false })
})

it('accessing a real file works', function (done) {
request(server)
.get('/users/tobi.txt')
.expect(200, 'ferret', done)
})

it('should 403 on nonexistant file', function (done) {
request(server)
.get('/users/bob.txt')
.expect(403, done)
})

it('should 403 on a symlink in the path', function (done) {
request(server)
.get('/members/tobi.txt')
.expect(403, done)
})

it('should 403 on a symlink as the file', function (done) {
request(server)
.get('/users/william.txt')
.expect(403, done)
})

it('should fail on nested root symlink', function (done) {
request(server)
.get('/symroot/users/tobi.txt')
.expect(403, done)
})
})

describe('when followsymlinks is false and root had symlinks', function () {
var server
before(function () {
server = createServer(fixtures + '/symroot', { followsymlinks: false, fallthrough: false })
})

it('accessing a real file works', function (done) {
request(server)
.get('/users/tobi.txt')
.expect(200, 'ferret', done)
})

it('should 403 on a symlink in the path', function (done) {
request(server)
.get('/members/tobi.txt')
.expect(403, done)
})

it('should 403 on a symlink as the file', function (done) {
request(server)
.get('/users/william.txt')
.expect(403, done)
})
})

describe('when followsymlinks is false and fallthrough is true', function () {
var server
before(function () {
server = createServer(fixtures, { followsymlinks: false, fallthrough: true })
})

it('accessing a real file works', function (done) {
request(server)
.get('/users/tobi.txt')
.expect(200, 'ferret', done)
})

it('should 404 on a symlink', function (done) {
request(server)
.get('/members/tobi.txt')
.expect(404, done)
})
})
})

describe('when responding non-2xx or 304', function () {
Expand Down