Audit fixes: harden Amber signEvent response handling, fix web AmberSigner signEvent naming, add Blossom upload fallbacks, sanitize untrusted URLs in FoodPage/CreateTourPage
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
5017c1b7ff
commit
5e8544316b
5 changed files with 70 additions and 10 deletions
|
|
@ -13,8 +13,13 @@ export function useUploadFile() {
|
|||
}
|
||||
|
||||
const uploader = new BlossomUploader({
|
||||
// Multiple servers so a single slow/down Blossom host doesn't hang the
|
||||
// upload (crew logo uploads were stalling when primal.net was slow).
|
||||
// The uploader tries them in order and falls back on failure.
|
||||
servers: [
|
||||
'https://blossom.primal.net/',
|
||||
'https://blossom.band/',
|
||||
'https://cdn.satellite.earth/',
|
||||
],
|
||||
signer: user.signer,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -604,7 +604,13 @@ export class AmberSigner implements NostrSigner {
|
|||
return this.pubkey;
|
||||
}
|
||||
|
||||
async sign(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent> {
|
||||
// NOTE: Nostrify's NostrSigner interface requires `signEvent` (not `sign`).
|
||||
// The old method was named `sign`, so every `user.signer.signEvent(...)` call
|
||||
// on a web/PWA Amber login threw "signer.signEvent is not a function". This
|
||||
// now correctly implements the interface. On web, signing genuinely requires
|
||||
// a redirect round-trip to Amber, so this navigates away and the result is
|
||||
// processed on return by AmberCallbackPage.
|
||||
async signEvent(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent> {
|
||||
// Store the current state and redirect to Amber
|
||||
// The result will be processed on page reload
|
||||
openAmberForSignEvent(event);
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ export class CapacitorAmberSigner implements NostrSigner {
|
|||
|
||||
async signEvent(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent> {
|
||||
const plugin = await getPlugin();
|
||||
const { event: signedJson } = await withTimeout(
|
||||
const res = await withTimeout(
|
||||
plugin.signEvent({
|
||||
packageName: AMBER_PACKAGE,
|
||||
eventJson: JSON.stringify(event),
|
||||
|
|
@ -211,7 +211,54 @@ export class CapacitorAmberSigner implements NostrSigner {
|
|||
}),
|
||||
'signature',
|
||||
);
|
||||
return JSON.parse(signedJson) as NostrEvent;
|
||||
|
||||
// Amber's response shape varies across builds: some return the full signed
|
||||
// event JSON in `event`, others return only `signature` (expecting the caller
|
||||
// to attach it to the original event). If we blindly `JSON.parse` an empty
|
||||
// `event`, or return an event with no `sig`, the resulting object silently
|
||||
// publishes as a malformed event and stalls the relay forever — the exact
|
||||
// "publish hangs" symptom. Handle every shape defensively and always fail
|
||||
// loud rather than emitting a bad event.
|
||||
let signed: Partial<NostrEvent> | null = null;
|
||||
|
||||
if (res.event && res.event.trim()) {
|
||||
try {
|
||||
signed = JSON.parse(res.event) as Partial<NostrEvent>;
|
||||
} catch {
|
||||
signed = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to reconstructing the event from a bare signature.
|
||||
if ((!signed || !signed.sig) && res.signature) {
|
||||
signed = { ...(signed ?? {}), sig: res.signature };
|
||||
}
|
||||
|
||||
if (!signed || !signed.sig) {
|
||||
throw new Error(
|
||||
'Amber returned an incomplete signature. Open Amber, approve the request, then try again.',
|
||||
);
|
||||
}
|
||||
|
||||
// Merge Amber's response over the original event so required fields are
|
||||
// never lost, then verify the essentials are present before returning.
|
||||
const merged: NostrEvent = {
|
||||
kind: signed.kind ?? event.kind,
|
||||
content: signed.content ?? event.content,
|
||||
tags: signed.tags ?? event.tags,
|
||||
created_at: signed.created_at ?? event.created_at,
|
||||
pubkey: signed.pubkey ?? this.pubkey,
|
||||
id: signed.id ?? '',
|
||||
sig: signed.sig,
|
||||
};
|
||||
|
||||
if (!merged.id || !merged.sig || !merged.pubkey) {
|
||||
throw new Error(
|
||||
'Amber returned a malformed event. Please retry, or restart Amber if it persists.',
|
||||
);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
readonly nip04 = {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { useNostrPublish } from '@/hooks/useNostrPublish';
|
|||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useUploadFile } from '@/hooks/useUploadFile';
|
||||
import { useShops } from '@/hooks/useShops';
|
||||
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
import { FeaturedUpgrade } from '@/components/FeaturedEventUpgrade';
|
||||
|
|
@ -1811,9 +1812,9 @@ export default function CreateTourPage() {
|
|||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||
{shop.website && (
|
||||
{shop.website && sanitizeUrl(shop.website) && (
|
||||
<a
|
||||
href={shop.website}
|
||||
href={sanitizeUrl(shop.website)!}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-cyan-400 hover:text-cyan-300"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
ChefHat,
|
||||
} from 'lucide-react';
|
||||
import type { FoodSpot } from '@/lib/types';
|
||||
import { sanitizeUrl, sanitizeTel } from '@/lib/sanitizeUrl';
|
||||
|
||||
const FOOD_TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
restaurant: ChefHat,
|
||||
|
|
@ -105,16 +106,16 @@ function FoodSpotCard({ spot }: { spot: FoodSpot }) {
|
|||
)}
|
||||
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
{spot.phone && (
|
||||
<a href={`tel:${spot.phone}`}>
|
||||
{spot.phone && sanitizeTel(spot.phone) && (
|
||||
<a href={sanitizeTel(spot.phone)!}>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Phone className="h-3 w-3" />
|
||||
Call
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
{spot.website && (
|
||||
<a href={spot.website} target="_blank" rel="noopener noreferrer">
|
||||
{spot.website && sanitizeUrl(spot.website) && (
|
||||
<a href={sanitizeUrl(spot.website)!} target="_blank" rel="noopener noreferrer">
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Globe className="h-3 w-3" />
|
||||
Website
|
||||
|
|
|
|||
Loading…
Reference in a new issue