Browse Source

block anything with stringified json in the content

imwald
Silberengel 1 month ago
parent
commit
34e6a80cd5
  1. 25
      src/lib/event-ingest-filter.ts
  2. 44
      src/services/client-events.service.ts
  3. 12
      src/services/client-query.service.ts
  4. 15
      src/services/client.service.ts

25
src/lib/event-ingest-filter.ts

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
import type { Event as NEvent } from 'nostr-tools'
/**
* Kinds whose `content` is conventionally a JSON object string per Nostr usage (not treated as app-protocol spam).
* Extend when another NIP documents JSON-in-content for a kind.
*/
export const NOSTR_KINDS_ALLOWED_STRINGIFIED_JSON_OBJECT_CONTENT = new Set<number>([0])
/**
* True when `content` is a stringified JSON **object** (not arrays/primitives) on a kind that should carry human text
* or other non-JSON payloads e.g. game/app data published as kind 31987 relay reviews.
*/
export function isStringifiedJsonObjectContentNostrEvent(
event: Pick<NEvent, 'kind' | 'content'>
): boolean {
if (NOSTR_KINDS_ALLOWED_STRINGIFIED_JSON_OBJECT_CONTENT.has(event.kind)) return false
const c = typeof event.content === 'string' ? event.content.trim() : ''
if (c.length < 2 || c[0] !== '{' || c[c.length - 1] !== '}') return false
try {
const v = JSON.parse(c) as unknown
return v !== null && typeof v === 'object' && !Array.isArray(v)
} catch {
return false
}
}

44
src/services/client-events.service.ts

@ -7,6 +7,7 @@ import indexedDb from './indexed-db.service' @@ -7,6 +7,7 @@ import indexedDb from './indexed-db.service'
import type { QueryService } from './client-query.service'
import client from './client.service'
import { isReplaceableEvent } from '@/lib/event'
import { isStringifiedJsonObjectContentNostrEvent } from '@/lib/event-ingest-filter'
import { buildComprehensiveRelayList } from '@/lib/relay-list-builder'
/**
@ -70,6 +71,17 @@ export class EventService { @@ -70,6 +71,17 @@ export class EventService {
return null
}
/** Returns cached event or undefined; evicts stringified-JSON-object spam from the session LRU. */
private getSessionEventIfAllowed(hexId: string): NEvent | undefined {
const e = this.sessionEventCache.get(hexId)
if (!e) return undefined
if (isStringifiedJsonObjectContentNostrEvent(e)) {
this.sessionEventCache.delete(hexId)
return undefined
}
return e
}
private notifySessionEventWaiters(hexId: string): void {
const waiters = this.sessionEventWaiters.get(hexId)
if (!waiters?.size) return
@ -90,7 +102,7 @@ export class EventService { @@ -90,7 +102,7 @@ export class EventService {
const hex = this.resolveHexWaiterKey(eventId)
if (!hex) return () => {}
if (this.sessionEventCache.has(hex)) {
if (this.getSessionEventIfAllowed(hex)) {
queueMicrotask(() => callback())
}
@ -134,16 +146,16 @@ export class EventService { @@ -134,16 +146,16 @@ export class EventService {
}
}
if (hexId) {
const fromSession = this.sessionEventCache.get(hexId)
const fromSession = this.getSessionEventIfAllowed(hexId)
if (fromSession) return fromSession
const cachedPromise = this.eventCacheMap.get(hexId)
if (cachedPromise) {
const resolved = await cachedPromise
if (resolved) return resolved
const fromSessionAfterMiss = this.sessionEventCache.get(hexId)
if (resolved && !isStringifiedJsonObjectContentNostrEvent(resolved)) return resolved
const fromSessionAfterMiss = this.getSessionEventIfAllowed(hexId)
if (fromSessionAfterMiss) return fromSessionAfterMiss
const fromDb = await indexedDb.getEventFromPublicationStore(hexId)
if (fromDb) {
if (fromDb && !isStringifiedJsonObjectContentNostrEvent(fromDb)) {
this.addEventToCache(fromDb)
return fromDb
}
@ -153,9 +165,12 @@ export class EventService { @@ -153,9 +165,12 @@ export class EventService {
}
const loaded = await this.eventDataLoader.load(hexId ?? trimmed)
if (hexId) {
const fromSessionAfter = this.sessionEventCache.get(hexId)
const fromSessionAfter = this.getSessionEventIfAllowed(hexId)
if (fromSessionAfter) return fromSessionAfter
}
if (loaded && isStringifiedJsonObjectContentNostrEvent(loaded)) {
return undefined
}
return loaded
}
@ -203,7 +218,7 @@ export class EventService { @@ -203,7 +218,7 @@ export class EventService {
.filter((id) => /^[0-9a-f]{64}$/.test(id))
)
]
const toFetch = hexIds.filter((id) => !this.sessionEventCache.has(id))
const toFetch = hexIds.filter((id) => !this.getSessionEventIfAllowed(id))
if (toFetch.length === 0) return
const relayUrls = await buildComprehensiveRelayListForEvents(undefined, [], [], [])
@ -303,6 +318,7 @@ export class EventService { @@ -303,6 +318,7 @@ export class EventService {
* Add event to session cache
*/
addEventToCache(event: NEvent): void {
if (isStringifiedJsonObjectContentNostrEvent(event)) return
const cleanEvent = { ...event }
delete (cleanEvent as any).relayStatuses
// REQ filters and nip19 decode use lowercase hex; some relays/clients emit uppercase ids.
@ -324,6 +340,7 @@ export class EventService { @@ -324,6 +340,7 @@ export class EventService {
const queryLower = query.toLowerCase()
for (const [, event] of this.sessionEventCache.entries()) {
if (isStringifiedJsonObjectContentNostrEvent(event)) continue
if (allowedKinds && !allowedKinds.includes(event.kind)) continue
const content = event.content.toLowerCase()
@ -419,7 +436,7 @@ export class EventService { @@ -419,7 +436,7 @@ export class EventService {
// Try cache first
if (filter.ids?.length) {
const cached = await indexedDb.getEventFromPublicationStore(filter.ids[0])
if (cached) {
if (cached && !isStringifiedJsonObjectContentNostrEvent(cached)) {
this.addEventToCache(cached)
// Extract relay hints from cached event's tags (e, a, q tags)
const eventRelayHints = this.extractRelayHintsFromEvent(cached)
@ -433,7 +450,7 @@ export class EventService { @@ -433,7 +450,7 @@ export class EventService {
// Try big relays first (uses user's inboxes + defaults)
if (filter.ids?.length) {
const event = await this.fetchEventFromBigRelaysDataloader.load(filter.ids[0])
if (event) {
if (event && !isStringifiedJsonObjectContentNostrEvent(event)) {
this.addEventToCache(event)
// Extract relay hints from found event's tags (e, a, q tags)
const eventRelayHints = this.extractRelayHintsFromEvent(event)
@ -446,7 +463,7 @@ export class EventService { @@ -446,7 +463,7 @@ export class EventService {
// Always try comprehensive relay list (author's outboxes + user's inboxes + hints + seen + defaults)
const event = await this.tryHarderToFetchEvent(relays, filter, true)
if (event) {
if (event && !isStringifiedJsonObjectContentNostrEvent(event)) {
this.addEventToCache(event)
return event
}
@ -455,7 +472,7 @@ export class EventService { @@ -455,7 +472,7 @@ export class EventService {
if (filter.ids?.length === 1) {
const raw = filter.ids[0]
const key = /^[0-9a-f]{64}$/i.test(raw) ? raw.toLowerCase() : raw
const sess = this.sessionEventCache.get(key)
const sess = this.getSessionEventIfAllowed(key)
if (sess) return sess
}
@ -505,7 +522,9 @@ export class EventService { @@ -505,7 +522,9 @@ export class EventService {
globalTimeout: isSingleEventById ? 12000 : 10000
})
const event = events.sort((a, b) => b.created_at - a.created_at)[0]
const event = events
.filter((e) => !isStringifiedJsonObjectContentNostrEvent(e))
.sort((a, b) => b.created_at - a.created_at)[0]
// For non-replaceable events, we've already returned immediately via immediateReturn
// But log it for debugging
@ -542,6 +561,7 @@ export class EventService { @@ -542,6 +561,7 @@ export class EventService {
const eventsMap = new Map<string, NEvent>()
for (const event of events) {
if (isStringifiedJsonObjectContentNostrEvent(event)) continue
const key = /^[0-9a-f]{64}$/i.test(event.id) ? event.id.toLowerCase() : event.id
eventsMap.set(key, event)
// Note: We can't track which relay returned which event in batch queries,

12
src/services/client-query.service.ts

@ -5,6 +5,7 @@ import { @@ -5,6 +5,7 @@ import {
MAX_CONCURRENT_RELAY_CONNECTIONS,
SEARCHABLE_RELAY_URLS
} from '@/constants'
import { isStringifiedJsonObjectContentNostrEvent } from '@/lib/event-ingest-filter'
import logger from '@/lib/logger'
import { normalizeUrl } from '@/lib/url'
import type { Filter, Event as NEvent } from 'nostr-tools'
@ -393,6 +394,13 @@ export class QueryService { @@ -393,6 +394,13 @@ export class QueryService {
return false
}
const forwardOnevent = callbacks.onevent
? (evt: NEvent) => {
if (isStringifiedJsonObjectContentNostrEvent(evt)) return
callbacks.onevent!(evt)
}
: undefined
const subs: { relayKey: string; close: () => void }[] = []
const allOpened = Promise.all(
groupedRequests.map(async ({ url, filters: relayFilters }, i) => {
@ -419,7 +427,7 @@ export class QueryService { @@ -419,7 +427,7 @@ export class QueryService {
const sub = relay.subscribe(relayFilters, {
receivedEvent: (_relay, id) => this.trackEventSeenOn(id, _relay),
onevent: (evt: NEvent) => callbacks.onevent?.(evt),
onevent: (evt: NEvent) => forwardOnevent?.(evt),
oneose: () => handleEose(i),
onclose: (reason: string) => {
releaseOnce()
@ -452,7 +460,7 @@ export class QueryService { @@ -452,7 +460,7 @@ export class QueryService {
try {
const sub2 = liveRelay.subscribe(relayFilters, {
receivedEvent: (_relay, id) => this.trackEventSeenOn(id, _relay),
onevent: (evt: NEvent) => callbacks.onevent?.(evt),
onevent: (evt: NEvent) => forwardOnevent?.(evt),
oneose: () => handleEose(i),
onclose: (reason2: string) => {
releaseSlot2()

15
src/services/client.service.ts

@ -17,6 +17,7 @@ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter { @@ -17,6 +17,7 @@ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter {
const { search: _search, ...rest } = f
return rest as Filter
}
import { isStringifiedJsonObjectContentNostrEvent } from '@/lib/event-ingest-filter'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import { dispatchTombstonesUpdated } from '@/lib/tombstone-events'
@ -1338,6 +1339,13 @@ class ClientService extends EventTarget { @@ -1338,6 +1339,13 @@ class ClientService extends EventTarget {
return false
}
const forwardOnevent = onevent
? (evt: NEvent) => {
if (isStringifiedJsonObjectContentNostrEvent(evt)) return
onevent(evt)
}
: undefined
const subs: { relayKey: string; close: () => void }[] = []
const allOpened = Promise.all(
groupedRequests.map(async ({ url, filters: relayFilters }, i) => {
@ -1367,7 +1375,7 @@ class ClientService extends EventTarget { @@ -1367,7 +1375,7 @@ class ClientService extends EventTarget {
onevent: (evt: NEvent) => {
logFirstEventIfFirstResponseWasEmpty(evt, relayKey)
logFirstRelayResponse('event', relayKey)
onevent?.(evt)
forwardOnevent?.(evt)
},
oneose: () => handleEose(i),
onclose: (reason: string) => {
@ -1408,7 +1416,7 @@ class ClientService extends EventTarget { @@ -1408,7 +1416,7 @@ class ClientService extends EventTarget {
onevent: (evt: NEvent) => {
logFirstEventIfFirstResponseWasEmpty(evt, relayKey)
logFirstRelayResponse('event', relayKey)
onevent?.(evt)
forwardOnevent?.(evt)
},
oneose: () => handleEose(i),
onclose: (reason2: string) => {
@ -1474,13 +1482,14 @@ class ClientService extends EventTarget { @@ -1474,13 +1482,14 @@ class ClientService extends EventTarget {
const customEvent = data as CustomEvent<NEvent>
const evt = customEvent.detail
if (!matchFilters(filters, evt)) return
if (isStringifiedJsonObjectContentNostrEvent(evt)) return
const id = evt.id
const have = _knownIds.has(id)
if (have) return
_knownIds.add(id)
onevent?.(evt)
forwardOnevent?.(evt)
}
this.addEventListener('newEvent', handleNewEventFromInternal)

Loading…
Cancel
Save