Skip to content

Commit a43b7b4

Browse files
vveerrggclaude
andcommitted
fix: relay URL validation and custom filter field allowlist
Validate relay URLs via Validation.isValidRelayUrl() before connection. Restrict custom filter fields to ALLOWED_FILTER_FIELDS allowlist to prevent injection of arbitrary keys into Nostr subscription filters. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 075c787 commit a43b7b4

2 files changed

Lines changed: 32 additions & 4 deletions

File tree

src/nodes/nostr-filter/nostr-filter.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ interface NostrFilterNode extends Node {
2828
hexPubkey?: string;
2929
}
3030

31+
// Allowed fields in a Nostr subscription filter (NIP-01 + NIP-12 tag filters)
32+
const ALLOWED_FILTER_FIELDS = new Set(['ids', 'authors', 'kinds', 'since', 'until', 'limit', 'search']);
33+
34+
function sanitizeFilterObject(parsed: Record<string, any>): Record<string, any> {
35+
const validated: Record<string, any> = {};
36+
for (const [key, value] of Object.entries(parsed)) {
37+
if (ALLOWED_FILTER_FIELDS.has(key) || key.startsWith('#')) {
38+
validated[key] = value;
39+
}
40+
}
41+
return validated;
42+
}
43+
3144
export default function(RED: NodeAPI) {
3245
// Create a function to initialize the node
3346
async function initializeNode(this: NostrFilterNode, config: NostrFilterDef) {
@@ -108,7 +121,8 @@ export default function(RED: NodeAPI) {
108121
case 'custom':
109122
if (this.customFilter) {
110123
try {
111-
const filter = JSON.parse(this.customFilter);
124+
const parsed = JSON.parse(this.customFilter);
125+
const filter = sanitizeFilterObject(parsed);
112126
shouldForward = Object.entries(filter).every(([key, value]) => {
113127
if (Array.isArray(value)) {
114128
return value.includes(event[key as keyof NostrEvent]);
@@ -158,7 +172,9 @@ export default function(RED: NodeAPI) {
158172
case 'custom':
159173
if (this.customFilter) {
160174
try {
161-
Object.assign(filter, JSON.parse(this.customFilter));
175+
const parsed = JSON.parse(this.customFilter);
176+
const validated = sanitizeFilterObject(parsed);
177+
Object.assign(filter, validated);
162178
} catch (err: any) {
163179
this.error("Invalid custom filter: " + err.message);
164180
}

src/nodes/nostr-relay-config/nostr-relay-config.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Node, NodeAPI, NodeDef } from 'node-red';
22
import { getDefaultReaderKeys, getPublicKey } from '../../crypto/keys.js';
3+
import { Validation } from '../../utils/validation.js';
34

45
// Types only - these won't be in the JS output
56
import type { NostrWSClient, NostrWSMessage } from 'nostr-websocket-utils';
@@ -24,8 +25,19 @@ export default function(RED: NodeAPI) {
2425
// Create a function to initialize the node
2526
async function initializeNode(this: NostrRelayConfig, config: NostrRelayConfigDef) {
2627
RED.nodes.createNode(this, config);
27-
28-
this.relay = config.relay;
28+
29+
// Validate relay URL before accepting
30+
if (config.relay) {
31+
if (!Validation.isValidRelayUrl(config.relay)) {
32+
this.error('Invalid relay URL: must use ws:// or wss:// protocol and be a valid URL');
33+
return;
34+
}
35+
this.relay = config.relay;
36+
} else {
37+
this.error('Relay URL is required');
38+
return;
39+
}
40+
2941
this.publicKey = config.publicKey;
3042
this.privateKey = this.credentials?.privateKey;
3143

0 commit comments

Comments
 (0)