@openclaw/nostr 2026.2.17 → 2026.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## 2026.2.17
3
+ ## 2026.2.19
4
4
 
5
5
  ### Changes
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nostr",
3
- "version": "2026.2.17",
3
+ "version": "2026.2.19",
4
4
  "description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -279,6 +279,23 @@ describe("nostr-profile-http", () => {
279
279
  expect(data.error).toContain("private");
280
280
  });
281
281
 
282
+ it("rejects ISATAP-embedded private IPv4 in picture URL", async () => {
283
+ const ctx = createMockContext();
284
+ const handler = createNostrProfileHttpHandler(ctx);
285
+ const req = createMockRequest("PUT", "/api/channels/nostr/default/profile", {
286
+ name: "hacker",
287
+ picture: "https://[2001:db8:1234::5efe:127.0.0.1]/evil.jpg",
288
+ });
289
+ const res = createMockResponse();
290
+
291
+ await handler(req, res);
292
+
293
+ expect(res._getStatusCode()).toBe(400);
294
+ const data = JSON.parse(res._getData());
295
+ expect(data.ok).toBe(false);
296
+ expect(data.error).toContain("private");
297
+ });
298
+
282
299
  it("rejects non-https URLs", async () => {
283
300
  const ctx = createMockContext();
284
301
  const handler = createNostrProfileHttpHandler(ctx);
@@ -8,7 +8,11 @@
8
8
  */
9
9
 
10
10
  import type { IncomingMessage, ServerResponse } from "node:http";
11
- import { readJsonBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk";
11
+ import {
12
+ isBlockedHostnameOrIp,
13
+ readJsonBodyWithLimit,
14
+ requestBodyErrorToText,
15
+ } from "openclaw/plugin-sdk";
12
16
  import { z } from "zod";
13
17
  import { publishNostrProfile, getNostrProfileState } from "./channel.js";
14
18
  import { NostrProfileSchema, type NostrProfile } from "./config-schema.js";
@@ -98,72 +102,6 @@ async function withPublishLock<T>(accountId: string, fn: () => Promise<T>): Prom
98
102
  // SSRF Protection
99
103
  // ============================================================================
100
104
 
101
- // Block common private/internal hostnames (quick string check)
102
- const BLOCKED_HOSTNAMES = new Set([
103
- "localhost",
104
- "localhost.localdomain",
105
- "127.0.0.1",
106
- "::1",
107
- "[::1]",
108
- "0.0.0.0",
109
- ]);
110
-
111
- // Check if an IP address (resolved) is in a private range
112
- function isPrivateIp(ip: string): boolean {
113
- // Handle IPv4
114
- const ipv4Match = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
115
- if (ipv4Match) {
116
- const [, a, b] = ipv4Match.map(Number);
117
- // 127.0.0.0/8 (loopback)
118
- if (a === 127) {
119
- return true;
120
- }
121
- // 10.0.0.0/8 (private)
122
- if (a === 10) {
123
- return true;
124
- }
125
- // 172.16.0.0/12 (private)
126
- if (a === 172 && b >= 16 && b <= 31) {
127
- return true;
128
- }
129
- // 192.168.0.0/16 (private)
130
- if (a === 192 && b === 168) {
131
- return true;
132
- }
133
- // 169.254.0.0/16 (link-local)
134
- if (a === 169 && b === 254) {
135
- return true;
136
- }
137
- // 0.0.0.0/8
138
- if (a === 0) {
139
- return true;
140
- }
141
- return false;
142
- }
143
-
144
- // Handle IPv6
145
- const ipLower = ip.toLowerCase().replace(/^\[|\]$/g, "");
146
- // ::1 (loopback)
147
- if (ipLower === "::1") {
148
- return true;
149
- }
150
- // fe80::/10 (link-local)
151
- if (ipLower.startsWith("fe80:")) {
152
- return true;
153
- }
154
- // fc00::/7 (unique local)
155
- if (ipLower.startsWith("fc") || ipLower.startsWith("fd")) {
156
- return true;
157
- }
158
- // ::ffff:x.x.x.x (IPv4-mapped IPv6) - extract and check IPv4
159
- const v4Mapped = ipLower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
160
- if (v4Mapped) {
161
- return isPrivateIp(v4Mapped[1]);
162
- }
163
-
164
- return false;
165
- }
166
-
167
105
  function validateUrlSafety(urlStr: string): { ok: true } | { ok: false; error: string } {
168
106
  try {
169
107
  const url = new URL(urlStr);
@@ -174,18 +112,7 @@ function validateUrlSafety(urlStr: string): { ok: true } | { ok: false; error: s
174
112
 
175
113
  const hostname = url.hostname.toLowerCase();
176
114
 
177
- // Quick hostname block check
178
- if (BLOCKED_HOSTNAMES.has(hostname)) {
179
- return { ok: false, error: "URL must not point to private/internal addresses" };
180
- }
181
-
182
- // Check if hostname is an IP address directly
183
- if (isPrivateIp(hostname)) {
184
- return { ok: false, error: "URL must not point to private/internal addresses" };
185
- }
186
-
187
- // Block suspicious TLDs that resolve to localhost
188
- if (hostname.endsWith(".localhost") || hostname.endsWith(".local")) {
115
+ if (isBlockedHostnameOrIp(hostname)) {
189
116
  return { ok: false, error: "URL must not point to private/internal addresses" };
190
117
  }
191
118