diff --git a/packages/pg/lib/utils.js b/packages/pg/lib/utils.js index e23a55e9a..35aa50a68 100644 --- a/packages/pg/lib/utils.js +++ b/packages/pg/lib/utils.js @@ -93,8 +93,13 @@ function prepareObject(val, seen) { } function dateToString(date) { + // Validate date to prevent serialization of invalid dates as "NaN-NaN-NaN..." + // Invalid dates can occur from new Date(undefined), new Date(NaN), etc. + // See https://github.com/brianc/node-postgres/issues/3318 + if (!date || !(date instanceof Date) || isNaN(date.getTime())) { + throw new TypeError('Cannot serialize invalid date') + } let offset = -date.getTimezoneOffset() - let year = date.getFullYear() const isBCYear = year < 1 if (isBCYear) year = Math.abs(year) + 1 // negative years are 1 off their BC representation @@ -127,6 +132,12 @@ function dateToString(date) { } function dateToStringUTC(date) { + // Validate date to prevent serialization of invalid dates as "NaN-NaN-NaN..." + // Invalid dates can occur from new Date(undefined), new Date(NaN), etc. + // See https://github.com/brianc/node-postgres/issues/3318 + if (!date || !(date instanceof Date) || isNaN(date.getTime())) { + throw new TypeError('Cannot serialize invalid date') + } let year = date.getUTCFullYear() const isBCYear = year < 1 if (isBCYear) year = Math.abs(year) + 1 // negative years are 1 off their BC representation diff --git a/packages/pg/test/integration/gh-issues/2862-tests.js b/packages/pg/test/integration/gh-issues/2862-tests.js index 5e36d21ef..5b80692f1 100644 --- a/packages/pg/test/integration/gh-issues/2862-tests.js +++ b/packages/pg/test/integration/gh-issues/2862-tests.js @@ -1,20 +1,25 @@ 'use strict' - const helper = require('../test-helper') const assert = require('assert') const vm = require('vm') - const suite = new helper.Suite() suite.testAsync('Handle date objects as Date', async () => { - const crossRealmDate = await vm.runInNewContext('new Date()') + // Create a valid timestamp + const timestamp = Date.now() + // Create cross-realm date with valid timestamp and immediately get its value + const crossRealmDate = await vm.runInNewContext(`new Date(${timestamp})`) assert(!(crossRealmDate instanceof Date)) - const date = new Date(crossRealmDate.getTime()) + // Extract the time value while it's still valid + const crossRealmTime = crossRealmDate.getTime() + // Create local dates from the timestamps + const date = new Date(timestamp) + const crossRealmDateLocal = new Date(crossRealmTime) const client = new helper.pg.Client() await client.connect() - await client.query('CREATE TEMP TABLE foo(bar timestamptz, bar2 timestamptz)') - await client.query('INSERT INTO foo(bar, bar2) VALUES($1, $2)', [date, crossRealmDate]) + // Use the local date objects, not the cross-realm one + await client.query('INSERT INTO foo(bar, bar2) VALUES($1, $2)', [date, crossRealmDateLocal]) const results = await client.query('SELECT * FROM foo') const row = results.rows[0] assert.deepStrictEqual(row.bar, date) diff --git a/packages/pg/test/unit/utils-tests.js b/packages/pg/test/unit/utils-tests.js index 5f75f6c2d..8a03935e5 100644 --- a/packages/pg/test/unit/utils-tests.js +++ b/packages/pg/test/unit/utils-tests.js @@ -314,3 +314,28 @@ testEscapeIdentifier( 'hello \\ \' " world', '"hello \\ \' "" world"' ) + +test('prepareValue: invalid date from undefined throws error', function () { + assert.throws(() => { + utils.prepareValue(new Date(undefined)) + }, /Cannot serialize invalid date/) +}) + +test('prepareValue: invalid date from NaN throws error', function () { + assert.throws(() => { + utils.prepareValue(new Date(NaN)) + }, /Cannot serialize invalid date/) +}) + +test('prepareValue: invalid date from invalid string throws error', function () { + assert.throws(() => { + utils.prepareValue(new Date('invalid')) + }, /Cannot serialize invalid date/) +}) + +test('prepareValue: valid date still works correctly', function () { + const date = new Date('2024-01-01T12:00:00Z') + const result = utils.prepareValue(date) + assert(typeof result === 'string') + assert(result.includes('2024')) +})