Skip to content

Commit 35f12a9

Browse files
feat: reject NIP-70 protected events
1 parent 54139ed commit 35f12a9

3 files changed

Lines changed: 145 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": minor
3+
---
4+
5+
feat: reject NIP-70 protected events and reposts embedding them

src/handlers/event-message-handler.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ContextMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base'
1+
import { ContextMetadataKey, EventExpirationTimeMetadataKey, EventKinds, EventTags } from '../constants/base'
22
import {
33
DEFAULT_NIP05_VERIFY_EXPIRATION_MS,
44
extractNip05FromEvent,
@@ -21,6 +21,7 @@ import {
2121
isEventSignatureValid,
2222
isExpiredEvent,
2323
isFileMessageEvent,
24+
isProtectedEvent,
2425
isRequestToVanishEvent,
2526
isSealEvent,
2627
isWelcomeRumorEvent,
@@ -88,6 +89,13 @@ export class EventMessageHandler implements IMessageHandler {
8889
return
8990
}
9091

92+
reason = this.isProtectedEventBlocked(event)
93+
if (reason) {
94+
logger('event %s rejected: %s', event.id, reason)
95+
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
96+
return
97+
}
98+
9199
reason = await this.isBlockedByRequestToVanish(event)
92100
if (reason) {
93101
logger('event %s rejected: %s', event.id, reason)
@@ -224,6 +232,25 @@ export class EventMessageHandler implements IMessageHandler {
224232
}
225233
}
226234

235+
protected isProtectedEventBlocked(event: Event): string | undefined {
236+
if (isProtectedEvent(event)) {
237+
return 'auth-required: this event may only be published by its author'
238+
}
239+
240+
if (event.kind === EventKinds.REPOST && event.content.length > 0) {
241+
try {
242+
const embedded = JSON.parse(event.content)
243+
if (
244+
Array.isArray(embedded?.tags) &&
245+
embedded.tags.some((tag: string[]) => Array.isArray(tag) && tag[0] === EventTags.Protected)
246+
) {
247+
return 'blocked: reposts must not embed protected events'
248+
}
249+
} catch (_e) {
250+
}
251+
}
252+
}
253+
227254
protected async isEventValid(event: Event): Promise<string | undefined> {
228255
if (!(await isEventIdValid(event))) {
229256
return 'invalid: event id does not match'

test/unit/handlers/event-message-handler.spec.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,4 +2120,116 @@ describe('EventMessageHandler', () => {
21202120
expect(nip05VerificationRepository.upsert).to.have.been.calledOnce
21212121
})
21222122
})
2123+
2124+
describe('isProtectedEventBlocked', () => {
2125+
beforeEach(() => {
2126+
handler = new EventMessageHandler(
2127+
{} as any,
2128+
() => null,
2129+
{} as any,
2130+
userRepository,
2131+
() =>
2132+
({
2133+
info: { relay_url: 'relay_url' },
2134+
}) as any,
2135+
{} as any,
2136+
{ hasKey: async () => false, setKey: async () => true } as any,
2137+
() => ({ hit: async () => false }),
2138+
)
2139+
})
2140+
2141+
it('returns reason if event has a protected tag', () => {
2142+
event.tags = [['-']]
2143+
expect((handler as any).isProtectedEventBlocked(event)).to.equal(
2144+
'auth-required: this event may only be published by its author',
2145+
)
2146+
})
2147+
2148+
it('returns undefined if event has no protected tag', () => {
2149+
event.tags = [['e', 'abc']]
2150+
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
2151+
})
2152+
2153+
it('returns undefined if event has no tags', () => {
2154+
event.tags = []
2155+
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
2156+
})
2157+
2158+
it('returns reason if kind 6 repost embeds a protected event', () => {
2159+
event.kind = EventKinds.REPOST
2160+
event.content = JSON.stringify({
2161+
id: 'a'.repeat(64),
2162+
pubkey: 'b'.repeat(64),
2163+
kind: 1,
2164+
tags: [['-']],
2165+
content: 'secret',
2166+
sig: 'c'.repeat(128),
2167+
created_at: 1000,
2168+
})
2169+
event.tags = []
2170+
expect((handler as any).isProtectedEventBlocked(event)).to.equal(
2171+
'blocked: reposts must not embed protected events',
2172+
)
2173+
})
2174+
2175+
it('returns undefined if kind 6 repost embeds a non-protected event', () => {
2176+
event.kind = EventKinds.REPOST
2177+
event.content = JSON.stringify({
2178+
id: 'a'.repeat(64),
2179+
pubkey: 'b'.repeat(64),
2180+
kind: 1,
2181+
tags: [],
2182+
content: 'public',
2183+
sig: 'c'.repeat(128),
2184+
created_at: 1000,
2185+
})
2186+
event.tags = []
2187+
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
2188+
})
2189+
2190+
it('returns undefined if kind 6 repost has empty content', () => {
2191+
event.kind = EventKinds.REPOST
2192+
event.content = ''
2193+
event.tags = []
2194+
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
2195+
})
2196+
2197+
it('returns undefined if kind 6 repost has invalid JSON content', () => {
2198+
event.kind = EventKinds.REPOST
2199+
event.content = 'not json'
2200+
event.tags = []
2201+
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
2202+
})
2203+
2204+
it('returns undefined for non-repost event kinds with JSON content', () => {
2205+
event.kind = EventKinds.TEXT_NOTE
2206+
event.content = JSON.stringify({ tags: [['-']] })
2207+
event.tags = []
2208+
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
2209+
})
2210+
2211+
it('rejects on the protected tag before checking embedded repost content', () => {
2212+
event.kind = EventKinds.REPOST
2213+
event.content = JSON.stringify({
2214+
id: 'a'.repeat(64),
2215+
pubkey: 'b'.repeat(64),
2216+
kind: 1,
2217+
tags: [['-']],
2218+
content: 'secret',
2219+
sig: 'c'.repeat(128),
2220+
created_at: 1000,
2221+
})
2222+
event.tags = [['-']]
2223+
expect((handler as any).isProtectedEventBlocked(event)).to.equal(
2224+
'auth-required: this event may only be published by its author',
2225+
)
2226+
})
2227+
2228+
it('returns undefined if kind 6 repost has non-array embedded tags', () => {
2229+
event.kind = EventKinds.REPOST
2230+
event.content = JSON.stringify({ tags: 'not-an-array' })
2231+
event.tags = []
2232+
expect((handler as any).isProtectedEventBlocked(event)).to.be.undefined
2233+
})
2234+
})
21232235
})

0 commit comments

Comments
 (0)