mirror of
https://github.com/hanzoai/session.git
synced 2026-09-24 17:02:35 +00:00
fix: use RFC 6265 path matching for cookie path option (#1114)
* test: add specific RFC 6265 5.1.4 compliance tests * feat: add specific RFC 6265 5.1.4 handler * docs: note RFC 6265 5.1.4 compliance in cookie.path documentation * feat: enhance cookie handling for RFC 6265 compliance with path matching and auto secure/sameSite options * feat: enforce RFC 6265 section 5.1.4 path matching for session cookie path --------- Co-authored-by: Sebastian Beltran <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Sebastian Beltran
parent
143d26eb9f
commit
672216dd12
@@ -1,3 +1,9 @@
|
||||
unreleased
|
||||
==========
|
||||
|
||||
* Enforce RFC 6265 section 5.1.4 path matching for the session cookie path
|
||||
* Fix `secure: 'auto'` and `sameSite: 'auto'` not being resolved when `cookie` is a function
|
||||
|
||||
1.19.0 / 2026-01-22
|
||||
==========
|
||||
|
||||
|
||||
@@ -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].
|
||||
@@ -1038,6 +1044,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
|
||||
|
||||
@@ -158,15 +158,16 @@ function session(options) {
|
||||
store.generate = function(req){
|
||||
req.sessionID = generateId(req);
|
||||
req.session = new Session(req);
|
||||
req.session.cookie = new Cookie(typeof cookieOptions === 'function' ? cookieOptions(req) : cookieOptions);
|
||||
var resolvedCookieOptions = typeof cookieOptions === 'function' ? cookieOptions(req) : cookieOptions;
|
||||
req.session.cookie = new Cookie(resolvedCookieOptions);
|
||||
|
||||
var isSecure = issecure(req, trustProxy);
|
||||
|
||||
if (cookieOptions.secure === 'auto') {
|
||||
if (resolvedCookieOptions.secure === 'auto') {
|
||||
req.session.cookie.secure = isSecure;
|
||||
}
|
||||
|
||||
if (cookieOptions.sameSite === 'auto') {
|
||||
if (resolvedCookieOptions.sameSite === 'auto') {
|
||||
req.session.cookie.sameSite = isSecure ? 'none' : 'lax';
|
||||
}
|
||||
};
|
||||
@@ -200,12 +201,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'));
|
||||
@@ -523,6 +527,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.
|
||||
*
|
||||
|
||||
+143
@@ -853,6 +853,40 @@ describe('session()', function(){
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set cookie when request-path does not match returned path', function (done) {
|
||||
var cookieCallback = function () {
|
||||
return { path: '/admin', httpOnly: true, secure: false };
|
||||
};
|
||||
var server = createServer({ cookie: cookieCallback });
|
||||
request(server)
|
||||
.get('/administrator')
|
||||
.expect(shouldNotHaveHeader('Set-Cookie'))
|
||||
.expect(200, done);
|
||||
});
|
||||
|
||||
it('should resolve "secure" and "sameSite" set to "auto"', function (done) {
|
||||
function setup (req) {
|
||||
req.secure = JSON.parse(req.headers['x-secure'])
|
||||
}
|
||||
|
||||
function respond (req, res) {
|
||||
res.end(String(req.secure))
|
||||
}
|
||||
|
||||
var cookieCallback = function () {
|
||||
return { path: '/', secure: 'auto', sameSite: 'auto' };
|
||||
};
|
||||
var server = createServer(setup, { cookie: cookieCallback }, respond);
|
||||
request(server)
|
||||
.get('/')
|
||||
.set('X-Secure', 'true')
|
||||
.expect(shouldSetCookieWithAttribute('connect.sid', 'Secure'))
|
||||
.expect(
|
||||
shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None')
|
||||
)
|
||||
.expect(200, 'true', done);
|
||||
});
|
||||
});
|
||||
describe('when "sameSite" set to "auto"', function () {
|
||||
describe('basic functionality', function () {
|
||||
@@ -2620,6 +2654,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;
|
||||
|
||||
Reference in New Issue
Block a user