Skip to content
Merged
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
4 changes: 3 additions & 1 deletion lib/web/cookies/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ function getCookies (headers) {

const cookie = headers.get('cookie')

// A null prototype keeps a cookie named `__proto__` from hitting the
// Object.prototype setter, which would silently drop it.
/** @type {Record<string, string>} */
const out = {}
const out = { __proto__: null }

if (!cookie) {
return out
Expand Down
23 changes: 21 additions & 2 deletions lib/web/cookies/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,27 @@ function stringify (cookie) {
out.push(`Path=${cookie.path}`)
}

if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') {
out.push(`Expires=${toIMFDate(cookie.expires)}`)
// A numeric 0 is the Unix epoch, not an absent value -- the same reason the
// Max-Age check above tests the type rather than truthiness.
//
// expires is Date | number, so the "Invalid Date" check only guards the Date
// form. A number that is not a valid time value (NaN, +/-Infinity, or outside
// the Date range) would otherwise reach toIMFDate, which splices undefined and
// NaN into the header value instead of failing.
if (cookie.expires != null) {
const expiresMs = cookie.expires instanceof Date
? cookie.expires.getTime()
: cookie.expires

// The largest time value a Date can represent, in milliseconds, i.e.
// 100,000,000 days either side of the epoch (ECMA-262 Date Time Values).
// Anything within it is safe for toIMFDate to format; anything beyond it
// produces an invalid Date and must be ignored instead.
const maxTimeValue = 8640000000000000

if (typeof expiresMs === 'number' && Number.isFinite(expiresMs) && Math.abs(expiresMs) <= maxTimeValue) {
out.push(`Expires=${toIMFDate(cookie.expires)}`)
}
}

if (cookie.sameSite) {
Expand Down
80 changes: 80 additions & 0 deletions test/cookie/cookies.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ test('Cookie parser', () => {
})
})

test('Cookie parser keeps a cookie named __proto__', () => {
const headers = new Headers()
headers.set('Cookie', '__proto__=foo; bar=baz')

const cookies = getCookies(headers)

assert.deepStrictEqual(Object.keys(cookies).sort(), ['__proto__', 'bar'])
assert.strictEqual(
Object.getOwnPropertyDescriptor(cookies, '__proto__').value,
'foo'
)
assert.strictEqual(cookies.bar, 'baz')

for (const value of Object.values(cookies)) {
assert.strictEqual(typeof value, 'string')
}
})

test('Cookie Name Validation', () => {
const tokens = [
'"id"',
Expand Down Expand Up @@ -379,6 +397,68 @@ test('Cookie Set', () => {
'Space=Cat; Expires=Fri, 07 Jan 1983 15:32:00 GMT'
)

// A numeric 0 is the Unix epoch, the canonical "expire now" value, not an
// absent option.
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
expires: 0
})
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Expires=Thu, 01 Jan 1970 00:00:00 GMT'
)

headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
expires: null
})
assert.equal(headers.get('Set-Cookie'), 'Space=Cat')

// A numeric expires outside the range representable by Date must not be
// serialized. Before this was guarded, the numeric form skipped the
// "Invalid Date" check and toIMFDate spliced undefined/NaN into the header.
for (const expires of [8640000000000001, 1e16, Number.MAX_SAFE_INTEGER]) {
headers = new Headers()
setCookie(headers, { name: 'Space', value: 'Cat', expires })
assert.equal(headers.get('Set-Cookie'), 'Space=Cat')
}

// The maximum representable time value is still serialized.
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
expires: 8640000000000000
})
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Expires=Sat, 13 Sep 275760 00:00:00 GMT'
)

// A non-finite number is coerced to 0 by the WebIDL converter before it
// reaches serialization, so it still serializes as the epoch.
for (const expires of [Infinity, -Infinity, NaN]) {
headers = new Headers()
setCookie(headers, { name: 'Space', value: 'Cat', expires })
assert.equal(
headers.get('Set-Cookie'),
'Space=Cat; Expires=Thu, 01 Jan 1970 00:00:00 GMT'
)
}

// An invalid Date is ignored, consistent with the out-of-range numeric cases.
headers = new Headers()
setCookie(headers, {
name: 'Space',
value: 'Cat',
expires: new Date(NaN)
})
assert.equal(headers.get('Set-Cookie'), 'Space=Cat')

headers = new Headers()
setCookie(headers, { name: '__Secure-Kitty', value: 'Meow' })
assert.equal(headers.get('Set-Cookie'), '__Secure-Kitty=Meow; Secure')
Expand Down
Loading