fix(nip98): reject tokens whose created_at is in the future

validateEventTimestamp() computed `now - created_at < 60`, which is
satisfied by any timestamp ahead of now, since the difference goes
negative. A token signed with a `created_at` far in the future therefore
validates indefinitely, and the 60 second window -- the only replay
defense NIP-98 has, as there is no nonce or seen-token cache -- never
closes for it.

Compare the absolute difference instead, so the window is 60 seconds on
both sides.
This commit is contained in:
Alex Gleason
2026-08-19 17:04:26 -03:00
committed by fiatjaf
parent 2ec294b6c1
commit c6d4311e0a
2 changed files with 25 additions and 2 deletions
+21
View File
@@ -136,6 +136,17 @@ describe('validateToken', () => {
expect(isTokenValid).rejects.toThrow(Error)
})
test('throws an error for an event timestamp in the future', async () => {
const sk = generateSecretKey()
const invalidToken = await getToken('http://test.com', 'get', e => {
e.created_at = Math.round(Date.now() / 1000) + 3600
return finalizeEvent(e, sk)
})
const isTokenValid = validateToken(invalidToken, 'http://test.com', 'get')
expect(isTokenValid).rejects.toThrow(Error)
})
test('throws an error for invalid url', async () => {
const sk = generateSecretKey()
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk))
@@ -239,6 +250,16 @@ describe('validateEventTimestamp', () => {
expect(isEventTimestampValid).toBe(false)
})
test('returns false for a timestamp in the future', async () => {
const sk = generateSecretKey()
const token = await getToken('http://test.com', 'get', e => finalizeEvent(e, sk), true)
const unpackedEvent: Event = await unpackEventFromToken(token)
unpackedEvent.created_at = Math.round(Date.now() / 1000) + 3600
const isEventTimestampValid = validateEventTimestamp(unpackedEvent)
expect(isEventTimestampValid).toBe(false)
})
})
describe('validateEventKind', () => {
+4 -2
View File
@@ -87,14 +87,16 @@ export async function unpackEventFromToken(token: string): Promise<Event> {
/**
* Validates the timestamp of an event.
* @param event - The event object to validate.
* @returns A boolean indicating whether the event timestamp is within the last 60 seconds.
* @returns A boolean indicating whether the event timestamp is within 60 seconds of now.
*/
export function validateEventTimestamp(event: Event): boolean {
if (!event.created_at) {
return false
}
return Math.round(new Date().getTime() / 1000) - event.created_at < 60
// the comparison has to be absolute: a `created_at` in the future produces a
// negative difference, which would otherwise satisfy the check forever.
return Math.abs(Math.round(new Date().getTime() / 1000) - event.created_at) < 60
}
/**