From e8456dab708fc0657d7e626da1ee1cb763bbd09f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 10 Sep 2025 10:40:06 -0300 Subject: [PATCH] khatru/policies: RejectUnprefixedNostrReferences --- khatru/policies/events.go | 26 +++++++++ khatru/policies/events_test.go | 89 ++++++++++++++++++++++++++++++ khatru/policies/strict_defaults.go | 1 + 3 files changed, 116 insertions(+) create mode 100644 khatru/policies/events_test.go diff --git a/khatru/policies/events.go b/khatru/policies/events.go index b08b123..e3b67a4 100644 --- a/khatru/policies/events.go +++ b/khatru/policies/events.go @@ -3,11 +3,13 @@ package policies import ( "context" "fmt" + "regexp" "slices" "strings" "time" "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip19" "fiatjaf.com/nostr/nip70" ) @@ -111,3 +113,27 @@ func OnlyAllowNIP70ProtectedEvents(ctx context.Context, event nostr.Event) (reje } return true, "blocked: we only accept events protected with the nip70 \"-\" tag" } + +func RejectUnprefixedNostrReferences(ctx context.Context, event nostr.Event) (bool, string) { + pattern := `\b(nevent1|npub1|nprofile1|note1)\w*\b` + re, err := regexp.Compile(pattern) + if err != nil { + // if regex error, allow + return false, "" + } + matches := re.FindAllStringIndex(event.Content, -1) + for _, match := range matches { + start := match[0] + end := match[1] + ref := event.Content[start:end] + _, _, err := nip19.Decode(ref) + if err != nil { + // invalid reference, ignore and allow + continue + } + if start < 6 || event.Content[start-6:start] != "nostr:" { + return true, "references must be prefixed with \"nostr:\"" + } + } + return false, "" +} diff --git a/khatru/policies/events_test.go b/khatru/policies/events_test.go new file mode 100644 index 0000000..3c60cdc --- /dev/null +++ b/khatru/policies/events_test.go @@ -0,0 +1,89 @@ +package policies + +import ( + "context" + "testing" + + "fiatjaf.com/nostr" +) + +func TestRejectUnprefixedNostrReferences(t *testing.T) { + tests := []struct { + name string + content string + shouldReject bool + }{ + { + name: "unprefixed nevent1 valid", + content: "nevent1qqsz3q79apv874xv8ta5za03nkmugnwc3nq046dd2wy30fh8hurn67qpp4mhxue69uhkummn9ekx7mqdzh53y", + shouldReject: true, + }, + { + name: "unprefixed npub1 valid", + content: "npub1eer3xzy76k8tqr2w40804d07qxyzq4ypfv0vv70kj3xnuukcdhts35cfkg", + shouldReject: true, + }, + { + name: "unprefixed nprofile1 valid", + content: "nprofile1qqsxu3ytjdwz9xwtlzuhrgf7yx3e0pcw8hvgtqnramc4t5gdh5vm6mgud3p0w", + shouldReject: true, + }, + { + name: "unprefixed note1 valid", + content: "note1sugf04s8yvveh7a4nhguhu2h3yumqd3kcr3yu6f4phk5u3m635wqz3tngh", + shouldReject: true, + }, + { + name: "prefixed nostr:nevent1", + content: "Check this event: nostr:nevent1qqsz3q79apv874xv8ta5za03nkmugnwc3nq046dd2wy30fh8hurn67qpp4mhxue69uhkummn9ekx7mqdzh53y", + shouldReject: false, + }, + { + name: "prefixed nostr:npub1", + content: "User: nostr:npub1eer3xzy76k8tqr2w40804d07qxyzq4ypfv0vv70kj3xnuukcdhts35cfkg", + shouldReject: false, + }, + { + name: "no references", + content: "This is just regular text", + shouldReject: false, + }, + { + name: "multiple unprefixed valid", + content: "See nevent1qqsz3q79apv874xv8ta5za03nkmugnwc3nq046dd2wy30fh8hurn67qpp4mhxue69uhkummn9ekx7mqdzh53y and npub1eer3xzy76k8tqr2w40804d07qxyzq4ypfv0vv70kj3xnuukcdhts35cfkg", + shouldReject: true, + }, + { + name: "mixed prefixed and unprefixed valid", + content: "Good: nostr:nevent1qqsz3q79apv874xv8ta5za03nkmugnwc3nq046dd2wy30fh8hurn67qpp4mhxue69uhkummn9ekx7mqdzh53y Bad: npub1eer3xzy76k8tqr2w40804d07qxyzq4ypfv0vv70kj3xnuukcdhts35cfkg", + shouldReject: true, + }, + { + name: "invalid unprefixed nevent1", + content: "nevent1abc123", + shouldReject: false, // invalid, so allowed + }, + { + name: "invalid unprefixed npub1", + content: "npub1def456", + shouldReject: false, // invalid, so allowed + }, + { + name: "invalid unprefixed note1", + content: "note1jkl012", + shouldReject: false, // invalid, so allowed + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := nostr.Event{ + Content: tt.content, + } + reject, _ := RejectUnprefixedNostrReferences(context.Background(), event) + if reject != tt.shouldReject { + t.Errorf("RejectUnprefixedNostrReferences() = %v, shouldReject %v", reject, tt.shouldReject) + } + }) + } +} diff --git a/khatru/policies/strict_defaults.go b/khatru/policies/strict_defaults.go index dbf3b58..af19691 100644 --- a/khatru/policies/strict_defaults.go +++ b/khatru/policies/strict_defaults.go @@ -11,6 +11,7 @@ var EventRejectionStrictDefaults = SeqEvent( PreventTooManyIndexableTags(12, []nostr.Kind{3}, nil), PreventTooManyIndexableTags(1200, nil, []nostr.Kind{3}), PreventLargeContent(5000), + RejectUnprefixedNostrReferences, EventIPRateLimiter(2, time.Minute*3, 10), )