Skip to content

Commit

Permalink
Merge 2759c85 into 94feedb
Browse files Browse the repository at this point in the history
  • Loading branch information
jayk committed Oct 22, 2019
2 parents 94feedb + 2759c85 commit 8f894b5
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 0 deletions.
14 changes: 14 additions & 0 deletions README.md
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
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 = require('constants')
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)
} 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
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 () {
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

0 comments on commit 8f894b5

Please sign in to comment.