Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/nip70-reject-protected-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostream": minor
---

feat: reject NIP-70 protected events and reposts embedding them
29 changes: 28 additions & 1 deletion src/handlers/event-message-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ContextMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base'
import { ContextMetadataKey, EventExpirationTimeMetadataKey, EventKinds, EventTags } from '../constants/base'
import {
DEFAULT_NIP05_VERIFY_EXPIRATION_MS,
extractNip05FromEvent,
Expand All @@ -21,6 +21,7 @@
isEventSignatureValid,
isExpiredEvent,
isFileMessageEvent,
isProtectedEvent,

Check failure on line 24 in src/handlers/event-message-handler.ts

View workflow job for this annotation

GitHub Actions / Build check

Module '"../utils/event"' has no exported member 'isProtectedEvent'.

Check failure on line 24 in src/handlers/event-message-handler.ts

View workflow job for this annotation

GitHub Actions / Build check

Module '"../utils/event"' has no exported member 'isProtectedEvent'.
isRequestToVanishEvent,
isSealEvent,
isWelcomeRumorEvent,
Expand Down Expand Up @@ -88,6 +89,13 @@
return
}

reason = this.isProtectedEventBlocked(event)
if (reason) {
logger('event %s rejected: %s', event.id, reason)
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
return
}

reason = await this.isBlockedByRequestToVanish(event)
if (reason) {
logger('event %s rejected: %s', event.id, reason)
Expand Down Expand Up @@ -224,6 +232,25 @@
}
}

protected isProtectedEventBlocked(event: Event): string | undefined {
if (isProtectedEvent(event)) {
return 'auth-required: this event may only be published by its author'
}

if (event.kind === EventKinds.REPOST && event.content.length > 0) {
try {
const embedded = JSON.parse(event.content)
if (
Array.isArray(embedded?.tags) &&
embedded.tags.some((tag: string[]) => Array.isArray(tag) && tag[0] === EventTags.Protected)

Check failure on line 245 in src/handlers/event-message-handler.ts

View workflow job for this annotation

GitHub Actions / Build check

Property 'Protected' does not exist on type 'typeof EventTags'.

Check failure on line 245 in src/handlers/event-message-handler.ts

View workflow job for this annotation

GitHub Actions / Build check

Property 'Protected' does not exist on type 'typeof EventTags'.
) {
return 'blocked: reposts must not embed protected events'
}
} catch (_e) {
}
}
}

protected async isEventValid(event: Event): Promise<string | undefined> {
if (!(await isEventIdValid(event))) {
return 'invalid: event id does not match'
Expand Down
112 changes: 112 additions & 0 deletions test/unit/handlers/event-message-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2120,4 +2120,116 @@ describe('EventMessageHandler', () => {
expect(nip05VerificationRepository.upsert).to.have.been.calledOnce
})
})

describe('isProtectedEventBlocked', () => {
beforeEach(() => {
handler = new EventMessageHandler(
{} as any,
() => null,
{} as any,
userRepository,
() =>
({
info: { relay_url: 'relay_url' },
}) as any,
{} as any,
{ hasKey: async () => false, setKey: async () => true } as any,
() => ({ hit: async () => false }),
)
})

it('returns reason if event has a protected tag', () => {
event.tags = [['-']]
expect((handler as any).isProtectedEventBlocked(event)).to.equal(
'auth-required: this event may only be published by its author',
)
})

it('returns undefined if event has no protected tag', () => {
event.tags = [['e', 'abc']]
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
})

it('returns undefined if event has no tags', () => {
event.tags = []
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
})

it('returns reason if kind 6 repost embeds a protected event', () => {
event.kind = EventKinds.REPOST
event.content = JSON.stringify({
id: 'a'.repeat(64),
pubkey: 'b'.repeat(64),
kind: 1,
tags: [['-']],
content: 'secret',
sig: 'c'.repeat(128),
created_at: 1000,
})
event.tags = []
expect((handler as any).isProtectedEventBlocked(event)).to.equal(
'blocked: reposts must not embed protected events',
)
})

it('returns undefined if kind 6 repost embeds a non-protected event', () => {
event.kind = EventKinds.REPOST
event.content = JSON.stringify({
id: 'a'.repeat(64),
pubkey: 'b'.repeat(64),
kind: 1,
tags: [],
content: 'public',
sig: 'c'.repeat(128),
created_at: 1000,
})
event.tags = []
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
})

it('returns undefined if kind 6 repost has empty content', () => {
event.kind = EventKinds.REPOST
event.content = ''
event.tags = []
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
})

it('returns undefined if kind 6 repost has invalid JSON content', () => {
event.kind = EventKinds.REPOST
event.content = 'not json'
event.tags = []
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
})

it('returns undefined for non-repost event kinds with JSON content', () => {
event.kind = EventKinds.TEXT_NOTE
event.content = JSON.stringify({ tags: [['-']] })
event.tags = []
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
})

it('rejects on the protected tag before checking embedded repost content', () => {
event.kind = EventKinds.REPOST
event.content = JSON.stringify({
id: 'a'.repeat(64),
pubkey: 'b'.repeat(64),
kind: 1,
tags: [['-']],
content: 'secret',
sig: 'c'.repeat(128),
created_at: 1000,
})
event.tags = [['-']]
expect((handler as any).isProtectedEventBlocked(event)).to.equal(
'auth-required: this event may only be published by its author',
)
})

it('returns undefined if kind 6 repost has non-array embedded tags', () => {
event.kind = EventKinds.REPOST
event.content = JSON.stringify({ tags: 'not-an-array' })
event.tags = []
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
})
})
})
Loading