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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ More information about can be found in [the proposal](https://github.com/privacy
Specifies the value for the `Path` `Set-Cookie`. By default, this is set to `'/'`, which
is the root path of the domain.

Since 1.19.1, path matching follows [RFC 6265 section 5.1.4][rfc-6265-5.1.4]. This means
the session middleware will only activate when the request path is an exact match or falls
under a segment boundary of the cookie path. For example, a cookie path of `/admin` will
match `/admin` and `/admin/users` but will **not** match `/administrator`. Prior versions
used a simple prefix check that did not enforce segment boundaries.

##### cookie.priority

Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1].
Expand Down Expand Up @@ -1048,6 +1054,7 @@ On Windows, use the corresponding command;

[MIT](LICENSE)

[rfc-6265-5.1.4]: https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
[rfc-6265bis-03-4.1.2.7]: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7
[rfc-cutler-httpbis-partitioned-cookies]: https://tools.ietf.org/html/draft-cutler-httpbis-partitioned-cookies/
[rfc-west-cookie-priority-00-4.1]: https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1
Expand Down
41 changes: 40 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,15 @@ function session(options) {
// pathname mismatch
var originalPath = parseUrl.original(req).pathname || '/'
var resolvedCookieOptions = typeof cookieOptions === 'function' ? cookieOptions(req) : cookieOptions
if (originalPath.indexOf(resolvedCookieOptions.path || '/') !== 0) {
var cfgPath = resolvedCookieOptions.path || '/'

if (!rfcPathMatch(originalPath, cfgPath)) {
debug('pathname mismatch')
next()
return
}


// ensure a secret is available or bail
if (!secret && !req.secret) {
next(new Error('secret option required for sessions'));
Expand Down Expand Up @@ -523,6 +526,42 @@ function session(options) {
};
};

/**
* Check if the cookiePath matches the requestPath following the
* rules in RFC 6265 section 5.1.4.
*
* @param {String} requestPath
* @param {String} cookiePath
* @return {Boolean}
* @private
*/

function rfcPathMatch(requestPath, cookiePath) {
// Normalize inputs (Node 0.8-safe)
requestPath = (typeof requestPath === 'string' && requestPath.length) ? requestPath : '/';
cookiePath = (typeof cookiePath === 'string' && cookiePath.length) ? cookiePath : '/';

// Root cookie matches everything
if (cookiePath === '/') return true;

// Exact match
if (requestPath === cookiePath) return true;

// Prefix match
if (requestPath.indexOf(cookiePath) === 0) {
// If cookiePath ends with '/', any longer requestPath is OK
if (cookiePath.charAt(cookiePath.length - 1) === '/') return true;

// Otherwise the next char after the prefix must be '/'
var nextChar = requestPath.length > cookiePath.length
? requestPath.charAt(cookiePath.length)
: '';
return nextChar === '/';
}

return false;
}

/**
* Generate a session ID for a new session.
*
Expand Down
109 changes: 109 additions & 0 deletions test/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -2620,6 +2620,115 @@ describe('session()', function(){
})
})

describe('path matching (RFC 6265)', function () {
describe('when "path" is "/" (root path)', function () {
before(function () {
this.server = createServer({ cookie: { path: '/' } })
})

it('should set cookie when request-path is "/" (root path)', function (done) {
// RFC 6265 5.1.4: "The cookie-path and the request-path are identical."
request(this.server)
.get('/')
.expect(shouldSetCookie('connect.sid'))
.expect(200, done)
})

it('should set cookie when request-path is any path ("/foo")', function (done) {
// RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last
// character of the cookie-path is %x2F ("/")."
request(this.server)
.get('/foo')
.expect(shouldSetCookie('connect.sid'))
.expect(200, done)
})

it('should set cookie when request-path has multiple segments ("/foo/bar/baz")', function (done) {
// RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last
// character of the cookie-path is %x2F ("/")."
request(this.server)
.get('/foo/bar/baz')
.expect(shouldSetCookie('connect.sid'))
.expect(200, done)
})
})

describe('when "path" is "/admin"', function () {
before(function () {
this.server = createServer({ cookie: { path: '/admin' } })
})

it('should set cookie when request-path and cookie-path are identical ("/admin")', function (done) {
// RFC 6265 5.1.4: "The cookie-path and the request-path are identical."
request(this.server)
.get('/admin')
.expect(shouldSetCookie('connect.sid'))
.expect(200, done)
})

it('should set cookie when cookie-path is prefix and last char is "/" ("/admin/")', function (done) {
// RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last
// character of the cookie-path is %x2F ("/")."
request(this.server)
.get('/admin/')
.expect(shouldSetCookie('connect.sid'))
.expect(200, done)
})

it('should set cookie when cookie-path is prefix and next char is "/" ("/admin/users")', function (done) {
// RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the first
// character of the request-path that is not included in the cookie-path is a %x2F ("/") character."
request(this.server)
.get('/admin/users')
.expect(shouldSetCookie('connect.sid'))
.expect(200, done)
})

it('should NOT set cookie when cookie-path is not a prefix ("/administrator")', function (done) {
// RFC 6265 5.1.4: None of the path-match conditions are met
request(this.server)
.get('/administrator')
.expect(shouldNotHaveHeader('Set-Cookie'))
.expect(200, done)
})
})

describe('when "path" is "/admin/" (trailing slash)', function () {
before(function () {
this.server = createServer({ cookie: { path: '/admin/' } })
})

it('should set cookie when cookie-path is prefix and last char is "/" ("/admin/x")', function (done) {
// RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last
// character of the cookie-path is %x2F ("/")."
request(this.server)
.get('/admin/x')
.expect(shouldSetCookie('connect.sid'))
.expect(200, done)
})

it('should NOT set cookie when request-path is not prefixed by cookie-path ("/admin")', function (done) {
// RFC 6265 5.1.4: cookie-path "/admin/" is not a prefix of request-path "/admin"
request(this.server)
.get('/admin')
.expect(shouldNotHaveHeader('Set-Cookie'))
.expect(200, done)
})

it('should NOT set cookie when cookie-path is not a prefix ("/administrator")', function (done) {
// RFC 6265 5.1.4: None of the path-match conditions are met:
// 1. The paths are not identical
// 2. "/admin/" is not a prefix of "/administrator"
// 3. The prefix condition with next character "/" is not applicable
request(this.server)
.get('/administrator')
.expect(shouldNotHaveHeader('Set-Cookie'))
.expect(200, done)
})
})
})


function cookie(res) {
var setCookie = res.headers['set-cookie'];
return (setCookie && setCookie[0]) || undefined;
Expand Down