fix(nostr): profile imports no longer crash on invalid fields (#110684)

* fix(nostr): profile imports no longer crash on invalid fields

* test(nostr): define invalid profile import policy

* test(nostr): cover invalid imported URL types

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
mushuiyu886
2026-07-18 17:18:23 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent b4187ced90
commit 9fe92cf5ce
2 changed files with 70 additions and 13 deletions
@@ -46,6 +46,20 @@ function createProfileEvent(overrides: Partial<Event> = {}): Event {
};
}
function respondWithProfileContent(content: unknown): void {
mockState.subscribeMany.mockImplementation((_relays, _filter, params) => {
params.onevent?.(createProfileEvent({ content: JSON.stringify(content) }));
params.oneose?.();
});
}
function importDefaultProfile() {
return importProfileFromRelays({
pubkey: "a".repeat(64),
relays: ["wss://relay.example"],
});
}
describe("nostr-profile-import", () => {
beforeEach(() => {
mockState.close.mockReset();
@@ -121,22 +135,55 @@ describe("nostr-profile-import", () => {
});
it("rejects profile content that is not a JSON object", async () => {
mockState.subscribeMany.mockImplementation((_relays, _filter, params) => {
params.onevent?.(createProfileEvent({ content: "null" }));
params.oneose?.();
});
respondWithProfileContent(null);
await expect(
importProfileFromRelays({
pubkey: "a".repeat(64),
relays: ["wss://relay.example"],
}),
).resolves.toMatchObject({
await expect(importDefaultProfile()).resolves.toMatchObject({
ok: false,
error: "Profile event content must be a JSON object",
sourceRelay: "wss://relay.example",
});
});
it.each([
{
case: "a wrong field type",
content: { name: 123, about: "valid" },
},
{
case: "a wrong URL field type",
content: { name: "valid", picture: 123 },
},
{
case: "an overlong field",
content: { name: "a".repeat(257), about: "valid" },
},
{
case: "a wrong field type alongside an unsafe URL",
content: { name: "valid", about: 123, picture: "https://127.0.0.1/avatar.png" },
},
])("rejects the whole profile for $case", async ({ content }) => {
respondWithProfileContent(content);
await expect(importDefaultProfile()).resolves.toMatchObject({
ok: false,
error: "Profile event content has invalid fields",
sourceRelay: "wss://relay.example",
});
});
it("drops unknown fields and unsafe URLs from an otherwise valid profile", async () => {
respondWithProfileContent({
name: "valid",
picture: "https://127.0.0.1/avatar.png",
website: "https://example.com",
custom: "ignored",
});
const result = await importDefaultProfile();
expect(result).toMatchObject({ ok: true, sourceRelay: "wss://relay.example" });
expect(result.profile).toStrictEqual({ name: "valid", website: "https://example.com" });
});
});
describe("mergeProfiles", () => {
+13 -3
View File
@@ -7,7 +7,7 @@
import { SimplePool, type Event } from "nostr-tools";
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
import type { NostrProfile } from "./config-schema.js";
import { type NostrProfile, NostrProfileSchema } from "./config-schema.js";
import { contentToProfile, type ProfileContent } from "./nostr-profile-core.js";
import { validateUrlSafety } from "./nostr-profile-url-safety.js";
@@ -187,12 +187,22 @@ export async function importProfileFromRelays(
// Convert to our profile format
const profile = contentToProfile(content);
// Sanitize URLs from imported profile to prevent SSRF when auto-merging
// Drop unsafe URLs before schema validation so an otherwise valid profile remains importable.
// Other invalid known fields reject the event atomically instead of silently changing its data.
const sanitizedProfile = sanitizeProfileUrls(profile);
const validatedProfile = NostrProfileSchema.safeParse(sanitizedProfile);
if (!validatedProfile.success) {
return {
ok: false,
error: "Profile event content has invalid fields",
relaysQueried,
sourceRelay: bestEvent.relay,
};
}
return {
ok: true,
profile: sanitizedProfile,
profile: validatedProfile.data,
event: {
id: bestEvent.event.id,
pubkey: bestEvent.event.pubkey,