security: verify relay events and stop image referrer leaks

This commit is contained in:
SkateSpots 2026-11-01 01:39:32 +07:00
parent b180d136c3
commit 7b2fd3993a
5 changed files with 8 additions and 4 deletions

View file

@ -14,7 +14,7 @@
Content-Security-Policy = "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' https: wss:; media-src 'self' blob: https:; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests" Content-Security-Policy = "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' https: wss:; media-src 'self' blob: https:; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests"
X-Frame-Options = "DENY" X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff" X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin" Referrer-Policy = "no-referrer"
Permissions-Policy = "geolocation=(self), camera=(self), microphone=()" Permissions-Policy = "geolocation=(self), camera=(self), microphone=()"
[[headers]] [[headers]]

View file

@ -2,7 +2,7 @@
Content-Security-Policy: default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' https: wss:; media-src 'self' blob: https:; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests Content-Security-Policy: default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' https: wss:; media-src 'self' blob: https:; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
X-Content-Type-Options: nosniff X-Content-Type-Options: nosniff
X-Frame-Options: DENY X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin Referrer-Policy: no-referrer
Permissions-Policy: geolocation=(self), camera=(self), microphone=() Permissions-Policy: geolocation=(self), camera=(self), microphone=()
# Cache the proxied mint directory so we don't hammer the rate-limited API. # Cache the proxied mint directory so we don't hammer the rate-limited API.

View file

@ -33,6 +33,7 @@ export function ImageWithFallback({
onError, onError,
loading = 'lazy', loading = 'lazy',
decoding = 'async', decoding = 'async',
referrerPolicy = 'no-referrer',
...props ...props
}: ImageWithFallbackProps) { }: ImageWithFallbackProps) {
const candidates = useMemo(() => imageCandidates(src), [src]); const candidates = useMemo(() => imageCandidates(src), [src]);
@ -67,6 +68,7 @@ export function ImageWithFallback({
className={className} className={className}
loading={loading} loading={loading}
decoding={decoding} decoding={decoding}
referrerPolicy={referrerPolicy}
onError={(event) => { onError={(event) => {
if (index + 1 < candidates.length) { if (index + 1 < candidates.length) {
setIndex(index + 1); setIndex(index + 1);

View file

@ -25,6 +25,7 @@ const AvatarImage = React.forwardRef<
<AvatarPrimitive.Image <AvatarPrimitive.Image
ref={ref} ref={ref}
className={cn("aspect-square h-full w-full object-cover", className)} className={cn("aspect-square h-full w-full object-cover", className)}
referrerPolicy="no-referrer"
{...props} {...props}
/> />
)) ))

View file

@ -6,6 +6,7 @@ import { useCitrine } from '@/hooks/useCitrine';
import { SKATE_RELAY } from '@/lib/types'; import { SKATE_RELAY } from '@/lib/types';
import { mergeEvents } from '@/lib/eventMerge'; import { mergeEvents } from '@/lib/eventMerge';
import { getLocalDeletions } from '@/lib/localDeletions'; import { getLocalDeletions } from '@/lib/localDeletions';
import { onlyVerified } from '@/lib/verifyEvents';
import type { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import type { NostrEvent, NostrFilter } from '@nostrify/nostrify';
/** /**
@ -139,7 +140,7 @@ export function useSkateRelays() {
throw new Error('No relay could be reached'); throw new Error('No relay could be reached');
} }
const allEvents = results.flat(); const allEvents = onlyVerified(results.flat());
return dedupeEvents(allEvents); return dedupeEvents(allEvents);
}; };
@ -161,7 +162,7 @@ export function useSkateRelays() {
const relay = nostr.relay(SKATE_RELAY); const relay = nostr.relay(SKATE_RELAY);
const events = await relay.query(filters, { signal: options.signal }); const events = await relay.query(filters, { signal: options.signal });
return dedupeEvents(events); return dedupeEvents(onlyVerified(events));
} catch (err) { } catch (err) {
// See querySkateContent: an outage must not look like "empty". // See querySkateContent: an outage must not look like "empty".
options.onError?.(); options.onError?.();