Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ function SendStream (req, path, options) {
? normalizeList(opts.extensions, 'extensions option')
: []

this._followSymlinks = opts.followSymlinks !== undefined
? Boolean(opts.followSymlinks)
: true

this._immutable = opts.immutable !== undefined
? Boolean(opts.immutable)
: false
Expand Down Expand Up @@ -488,6 +492,50 @@ SendStream.prototype.pipe = function pipe (res) {
*/

SendStream.prototype.send = function send (path, stat) {
var self = this
var root = this._root

if (this.res.headersSent) {
// impossible to send now
this.headersAlreadySent()
return
}

if (!this._followSymlinks && root !== null) {
debug('check symlink "%s"', path)
fs.realpath(path, function onrealpath (err, realPath) {
if (err) {
return self.onStatError(err)
}

fs.realpath(root, function onrealroot (err, realRoot) {
if (err) {
return self.onStatError(err)
}

if (realPath !== realRoot && !realPath.startsWith(realRoot.endsWith(sep) ? realRoot : realRoot + sep)) {
debug('symlink target outside root "%s"', realPath)
return self.error(403)
}

self._send(path, stat)
})
})
return
}

this._send(path, stat)
}

/**
* Transfer `path` to response.
*
* @param {String} path
* @param {Object} stat
* @api private
*/

SendStream.prototype._send = function _send (path, stat) {
var len = stat.size
var options = this.options
var opts = {}
Expand Down
95 changes: 95 additions & 0 deletions test/send.js
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,101 @@ describe('send(file, options)', function () {
})
})

describe('followSymlinks', function () {
var petsDir = path.join(fixtures, 'pets')
var linkDir = path.join(petsDir, 'outside-dir')
var linkFile = path.join(petsDir, 'outside-file.txt')
var linkInside = path.join(fixtures, 'inside-symlink.txt')
var hasFileSymlink = false
var hasDirSymlink = false
var hasInsideSymlink = false

before(function () {
try {
fs.symlinkSync(path.join(fixtures, 'name.txt'), linkFile)
hasFileSymlink = true
} catch (e) {
// file symlink creation may not be permitted on Windows without privileges
}

try {
fs.symlinkSync(path.join(fixtures, 'name.txt'), linkInside)
hasInsideSymlink = true
} catch (e) {
// file symlink creation may not be permitted on Windows without privileges
}

try {
var linkType = process.platform === 'win32' ? 'junction' : 'dir'
fs.symlinkSync(fixtures, linkDir, linkType)
hasDirSymlink = true
} catch (e) {
// ignore
}
})

after(function () {
if (hasFileSymlink) {
try {
fs.unlinkSync(linkFile)
} catch (e) {
try { fs.rmdirSync(linkFile) } catch (e) {}
}
}
if (hasDirSymlink) {
try {
fs.unlinkSync(linkDir)
} catch (e) {
try { fs.rmdirSync(linkDir) } catch (e) {}
}
}
if (hasInsideSymlink) {
try {
fs.unlinkSync(linkInside)
} catch (e) {
try { fs.rmdirSync(linkInside) } catch (e) {}
}
}
})

it('should default to true', function (done) {
if (!hasFileSymlink && !hasDirSymlink) return this.skip()
var targetUrl = hasFileSymlink ? '/outside-file.txt' : '/outside-dir/name.txt'
request(createServer({ root: petsDir }))
.get(targetUrl)
.expect(200, 'tobi', done)
})

it('should allow symlinks pointing outside root when true', function (done) {
if (!hasFileSymlink && !hasDirSymlink) return this.skip()
var targetUrl = hasFileSymlink ? '/outside-file.txt' : '/outside-dir/name.txt'
request(createServer({ followSymlinks: true, root: petsDir }))
.get(targetUrl)
.expect(200, 'tobi', done)
})

it('should 403 for symlinks pointing outside root when false', function (done) {
if (!hasFileSymlink && !hasDirSymlink) return this.skip()
var targetUrl = hasFileSymlink ? '/outside-file.txt' : '/outside-dir/name.txt'
request(createServer({ followSymlinks: false, root: petsDir }))
.get(targetUrl)
.expect(403, done)
})

it('should allow symlinks pointing inside root when false', function (done) {
if (!hasInsideSymlink) return this.skip()
request(createServer({ followSymlinks: false, root: fixtures }))
.get('/inside-symlink.txt')
.expect(200, 'tobi', done)
})

it('should allow regular files within root when false', function (done) {
request(createServer({ followSymlinks: false, root: petsDir }))
.get('/index.html')
.expect(200, done)
})
})

describe('lastModified', function () {
it('should support disabling last-modified', function (done) {
request(createServer({ lastModified: false, root: fixtures }))
Expand Down