From 88414a0917ceb9d64ff381bc53cf7839433a1ec6 Mon Sep 17 00:00:00 2001 From: Yang Xia Date: Wed, 22 Jul 2026 13:28:56 -0700 Subject: [PATCH] fix(gremlin-javascript): Reject out-of-range OffsetDateTime values Validate the constructed Date in the GraphBinary and GraphSON OffsetDateTime readers and throw when it falls outside the range representable by a JavaScript Date, instead of silently returning an invalid Date. https://issues.apache.org/jira/browse/TINKERPOP-3276 Assisted-by: Kiro:Claude-Opus-4.8 --- CHANGELOG.asciidoc | 1 + .../internals/OffsetDateTimeSerializer.js | 9 ++ .../lib/structure/io/type-serializers.js | 10 +- .../OffsetDateTimeSerializer-test.js | 118 ++++++++++++++++++ .../test/unit/graphson-test.js | 16 +++ 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/OffsetDateTimeSerializer-test.js diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index a5ecd33da38..41873227ada 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -32,6 +32,7 @@ This release also includes changes from prior 3.7.x releases. * Bumped Hadoop to 3.4.3 (and Kerby to 2.0.3) to enable `hadoop-gremlin` to build and run on Java 25. * Add missing `Configuring` interface to `GraphStepPlaceholder` and `VertexStepPlaceholder` * Fixed bug in `group()` value traversal where keys were retained with stale barrier state instead of being filtered when steps following a `Barrier` in the second `by()` produced no output (e.g. `by(values("age").fold().unfold())` or `by(__.out().fold().count(local).is(P.gt(0)))` for vertices with no out-edges). +* Fixed bug in `gremlin-javascript` GraphBinary and GraphSON deserialization where `OffsetDateTime` values outside the JavaScript `Date` range were silently returned as invalid `Date` objects instead of failing deserialization. [[release-3-8-1]] === TinkerPop 3.8.1 (Release Date: April 1, 2026) diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/OffsetDateTimeSerializer.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/OffsetDateTimeSerializer.js index 30045ffd0ff..0a30855d293 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/OffsetDateTimeSerializer.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/OffsetDateTimeSerializer.js @@ -143,6 +143,15 @@ module.exports = class OffsetDateTimeSerializer { // use UTC time calculated with offset above const v = new Date(Date.UTC(year, month, date, h, m, s, ms)); + // The GraphBinary DateTime/OffsetDateTime wire format can carry values (e.g. extreme years near + // +/-999999999) that fall outside the range representable by a JavaScript Date. In those cases + // Date.UTC(...) returns NaN and new Date(NaN) yields an invalid Date without throwing. Reject such + // values here so unsupported boundary date-times fail deserialization instead of silently producing + // an unusable Date instance. + if (Number.isNaN(v.getTime())) { + throw new Error('{value} is outside the range supported by JavaScript Date'); + } + return { v, len }; } catch (err) { throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/type-serializers.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/type-serializers.js index 7f5a392fd6e..a3a683660cd 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/type-serializers.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/type-serializers.js @@ -103,7 +103,15 @@ class OffsetDateTimeSerializer extends TypeSerializer { } deserialize(obj) { - return new Date(obj[valueKey]); + // An OffsetDateTime value can carry a date-time (e.g. extreme years) that falls outside the range + // representable by a JavaScript Date. In those cases new Date(...) yields an invalid Date without + // throwing. Reject such values so unsupported boundary date-times fail deserialization instead of + // silently producing an unusable Date instance. Mirrors the GraphBinary OffsetDateTime reader. + const value = new Date(obj[valueKey]); + if (Number.isNaN(value.getTime())) { + throw new Error('OffsetDateTime value is outside the range supported by JavaScript Date'); + } + return value; } canBeUsedFor(value) { diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/OffsetDateTimeSerializer-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/OffsetDateTimeSerializer-test.js new file mode 100644 index 00000000000..f854fad3512 --- /dev/null +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/OffsetDateTimeSerializer-test.js @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +'use strict'; + +const utils = require('./utils'); +const assert = require('assert'); +const ioc = require('../../../lib/structure/io/binary/GraphBinary'); + +const { from, concat } = Buffer; + +const ID = 0x88; // OFFSETDATETIME + +describe('GraphBinary.OffsetDateTimeSerializer', () => { + + const type_code = from([ID]); + const value_flag = from([0x00]); + + const serializer = ioc.serializers[ID]; + + const cases = [ + { v: undefined, fq: 1, b: [ID, 0x01], av: null }, + { v: null, fq: 1, b: [ID, 0x01] }, + + // year=2022(0x07e6), month=5, day=1, ns=0x000042c277bd8e00, offset=0 + { v: new Date(1651436603000), + b: [0x00,0x00,0x07,0xe6, 0x05, 0x01, 0x00,0x00,0x42,0xc2,0x77,0xbd,0x8e,0x00, 0x00,0x00,0x00,0x00] }, + + { des: 1, err: /buffer is missing/, fq: 1, b: undefined }, + { des: 1, err: /buffer is empty/, fq: 1, b: [] }, + { des: 1, err: /unexpected {type_code}/, fq: 1, b: [ID - 1] }, + { des: 1, err: /{value_flag} is missing/, fq: 1, b: [ID] }, + { des: 1, err: /unexpected {value_flag}/, fq: 1, b: [ID, 0x02] }, + { des: 1, err: /unexpected {value} length/, fq: 1, b: [ID, 0x00] }, + + // Boundary values that fall outside the JavaScript Date range (year=999999999=0x3B9AC9FF) + // must be rejected rather than silently deserialized into an invalid Date. See TINKERPOP-3276. + { des: 1, err: /outside the range supported by JavaScript Date/, fq: 0, + b: [0x3B,0x9A,0xC9,0xFF, 0x01, 0x01, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00] }, + // year=-999999999=0xC4653601 + { des: 1, err: /outside the range supported by JavaScript Date/, fq: 0, + b: [0xC4,0x65,0x36,0x01, 0x01, 0x01, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00] }, + ]; + + describe('#serialize', () => + cases + .filter(({ des }) => !des) + .forEach(({ v, fq, b }, i) => it(utils.ser_title({ i, v }), () => { + b = from(b); + + if (fq !== undefined) { + assert.deepEqual(serializer.serialize(v, fq), b); + return; + } + + assert.deepEqual(serializer.serialize(v, true), concat([type_code, value_flag, b])); + assert.deepEqual(serializer.serialize(v, false), concat([b])); + })) + ); + + describe('#deserialize', () => + cases.forEach(({ v, fq, b, av, err }, i) => it(utils.des_title({ i, b }), () => { + if (Array.isArray(b)) + b = from(b); + + if (err !== undefined) { + if (fq !== undefined) + assert.throws(() => serializer.deserialize(b, fq), { message: err }); + else { + assert.throws(() => serializer.deserialize(concat([type_code, value_flag, b]), true), { message: err }); + assert.throws(() => serializer.deserialize(concat([b]), false), { message: err }); + } + return; + } + + if (av !== undefined) + v = av; + const len = b.length; + + if (fq !== undefined) { + assert.deepStrictEqual(serializer.deserialize(b, fq), { v, len }); + return; + } + + assert.deepStrictEqual(serializer.deserialize(concat([type_code, value_flag, b]), true), { v, len: len + 2 }); + assert.deepStrictEqual(serializer.deserialize(concat([b]), false), { v, len: len + 0 }); + })) + ); + + describe('#canBeUsedFor', () => + [ + { v: null, e: false }, + { v: undefined, e: false }, + { v: {}, e: false }, + { v: [], e: false }, + { v: new Date(), e: true }, + ].forEach(({ v, e }, i) => it(utils.cbuf_title({ i, v }), () => + assert.strictEqual(serializer.canBeUsedFor(v), e) + )) + ); + +}); diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js index 77693067049..f968c352304 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js @@ -90,6 +90,22 @@ describe('GraphSONReader', function () { const result = reader.read(obj); assert.ok(result instanceof Date); }); + it('should parse OffsetDateTime', function() { + const obj = { "@type" : "gx:OffsetDateTime", "@value" : "2016-12-14T21:14:36.295Z" }; + const reader = new GraphSONReader(); + const result = reader.read(obj); + assert.ok(result instanceof Date); + assert.strictEqual(result.getTime(), 1481750076295); + }); + it('should reject OffsetDateTime outside the JavaScript Date range', function() { + const reader = new GraphSONReader(); + [ + { "@type" : "gx:OffsetDateTime", "@value" : "+999999-01-01T00:00:00Z" }, + { "@type" : "gx:OffsetDateTime", "@value" : "-999999-01-01T00:00:00Z" }, + ].forEach(function (obj) { + assert.throws(() => reader.read(obj), /outside the range supported by JavaScript Date/); + }); + }); it('should parse vertices from GraphSON', function () { const obj = { "@type":"g:Vertex","@value":{"id":{"@type":"g:Int32","@value":1},"label":"person",