Skip to content

fix(cookies): ignore a numeric expires outside the Date range - #5832

Open
xia-chao wants to merge 1 commit into
nodejs:mainfrom
xia-chao:fix/cookies-numeric-expires-out-of-range
Open

xia-chao wants to merge 1 commit into
nodejs:mainfrom
xia-chao:fix/cookies-numeric-expires-out-of-range

Conversation

@xia-chao

Copy link
Copy Markdown

What this fixes

setCookie produced a broken Expires attribute when expires was a number outside the range a Date can hold:

setCookie(headers, { name: 'Space', value: 'Cat', expires: 1e16 })
// before: Space=Cat; Expires=undefined, undefined undefined NaN undefined:undefined:undefined GMT
// after:  Space=Cat

Fixes #5831.

Why

expires can be a Date or a number, but the check in stringify only understood the Date form:

if (cookie.expires != null && cookie.expires.toString() !== 'Invalid Date') {

A bad Date becomes the text "Invalid Date", so it gets skipped. A number never does - NaN.toString() is "NaN" and (1e16).toString() is "10000000000000000" - so the check always passed and the value reached toIMFDate, which pasted undefined/NaN straight into the header.

The change

Check the number itself, and leave the Date behaviour exactly as it was:

if (cookie.expires != null) {
  const expiresMs = cookie.expires instanceof Date
    ? cookie.expires.getTime()
    : cookie.expires

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

8640000000000000 is the largest value a Date can hold, so anything passing the check is safe to format. This is the same approach the Max-Age branch a few lines above already uses.

Infinity, -Infinity and NaN are turned into 0 by the converter in lib/web/cookies/index.js before this code runs, so they still serialize as the epoch. That is a separate problem and this change does not touch it.

Tests

Added cases in test/cookie/cookies.js, next to the existing numeric-expires test:

  • out-of-range numbers (8640000000000001, 1e16, Number.MAX_SAFE_INTEGER) omit the attribute
  • the largest valid value 8640000000000000 still serializes
  • Infinity, -Infinity, NaN still serialize as the epoch
  • an invalid Date is still omitted
node --test test/cookie/*.js test/utils/date.js
# 99 passing, 0 failing

Reverting only lib/web/cookies/util.js makes the new cases fail with:

AssertionError: 'Space=Cat; Expires=undefined, undefined undefined NaN undefined:undefined:undefined GMT' == 'Space=Cat'

npx standard lib/web/cookies/util.js test/cookie/cookies.js is clean.

Comment thread lib/web/cookies/util.js Outdated
? cookie.expires.getTime()
: cookie.expires

if (typeof expiresMs === 'number' && Number.isFinite(expiresMs) && Math.abs(expiresMs) <= 8640000000000000) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just add a comment of why that harcoded number

@codecov-commenter

codecov-commenter commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.54%. Comparing base (6933139) to head (cd66bb8).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5832   +/-   ##
=======================================
  Coverage   93.54%   93.54%           
=======================================
  Files         110      110           
  Lines       39596    39607   +11     
=======================================
+ Hits        37040    37051   +11     
  Misses       2556     2556           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

stringify guarded expires with a string comparison that only works for the
Date form:

    if (cookie.expires != null && cookie.expires.toString() !== 'Invalid Date')

A Date whose time value is NaN stringifies to "Invalid Date" and is skipped,
but a number never does: NaN.toString() is "NaN" and 1e16.toString() is
"10000000000000000". The guard therefore always passes for a number outside
the range representable by Date, and toIMFDate reads NaN/undefined from every
getter and splices those tokens into the header value:

    setCookie(headers, { name: 'Space', value: 'Cat', expires: 1e16 })
    // Space=Cat; Expires=undefined, undefined undefined NaN undefined:undefined:undefined GMT

The emitted value is not a valid IMF-fixdate. The Max-Age branch a few lines
above already tests the type rather than a stringified value.

Treat a number as absent only when it is not a valid time value, keeping the
existing Date behaviour. Name the upper bound so it is clear it is the largest
time value a Date can represent rather than a magic number.

Non-finite numbers reach stringify already coerced to 0 by the unsigned long
long converter in lib/web/cookies/index.js, so they keep serializing as the
epoch; that coercion is a separate concern.

Signed-off-by: Xia Chao <236466140+bun-unsafe@users.noreply.github.com>
@xia-chao
xia-chao force-pushed the fix/cookies-numeric-expires-out-of-range branch from cd66bb8 to 2bbba02 Compare September 17, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

setCookie writes a broken Expires header when expires is an out-of-range number

4 participants