fix(ui): sanitize WireGuard QR SVG, guard mesh poll interval, log catalog fetch failures

Server.vue rendered the backend-generated WireGuard peer QR with raw
v-html while the analogous TOTP QR was DOMPurify-sanitized — both now
use the same svg-profile sanitizer. Mesh.vue's 5s poll interval gets
the same start-guard as the arch poll (no leak on double-mount) and is
nulled on unmount. curatedApps.ts catalog fetches no longer fail
silently: each failed source logs a console.warn, including the final
all-sources-failed fallback to the hardcoded list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-04 09:02:00 -04:00
parent 4c75bb3d38
commit 2f20ba8148
3 changed files with 26 additions and 12 deletions

View File

@ -388,13 +388,15 @@ onMounted(async () => {
if (!archPollInterval) {
archPollInterval = setInterval(loadArchMessages, 15000)
}
pollInterval = setInterval(() => {
mesh.fetchStatus()
mesh.fetchPeers()
mesh.fetchMessages()
mesh.fetchDeadmanStatus()
mesh.fetchBlockHeaders()
}, 5000)
if (!pollInterval) {
pollInterval = setInterval(() => {
mesh.fetchStatus()
mesh.fetchPeers()
mesh.fetchMessages()
mesh.fetchDeadmanStatus()
mesh.fetchBlockHeaders()
}, 5000)
}
// Instant peer updates (#48): the backend nudges the data-model revision when
// it discovers/updates a mesh peer, so refetch peers on the WS push rather
@ -414,7 +416,7 @@ onUnmounted(() => {
window.visualViewport.removeEventListener('scroll', updateKeyboardInset)
}
document.documentElement.style.removeProperty('--keyboard-inset')
if (pollInterval) clearInterval(pollInterval)
if (pollInterval) { clearInterval(pollInterval); pollInterval = null }
if (archPollInterval) { clearInterval(archPollInterval); archPollInterval = null }
if (wsUnsub) { wsUnsub(); wsUnsub = null }
})

View File

@ -306,7 +306,7 @@
</div>
<!-- Existing peer QR view -->
<div v-else-if="peerQrData && !showingNewDevice" class="text-center">
<div class="bg-white rounded-xl p-4 mb-4 inline-block" v-html="peerQrData.qr_svg"></div>
<div class="bg-white rounded-xl p-4 mb-4 inline-block" v-html="sanitizedPeerQrSvg"></div>
<p class="text-sm text-white/70 mb-2">Scan with the <strong>WireGuard</strong> app</p>
<p class="text-xs text-white/40 font-mono mb-4">{{ peerQrData.peer_ip }}</p>
<div class="flex gap-2">
@ -318,7 +318,7 @@
<div v-else>
<div v-if="peerQrData">
<div class="text-center">
<div class="bg-white rounded-xl p-4 mb-4 inline-block" v-html="peerQrData.qr_svg"></div>
<div class="bg-white rounded-xl p-4 mb-4 inline-block" v-html="sanitizedPeerQrSvg"></div>
<p class="text-sm text-white/70 mb-2">Scan with the <strong>WireGuard</strong> app</p>
<p class="text-xs text-white/40 font-mono mb-4">{{ peerQrData.peer_ip }}</p>
<div class="flex gap-2">
@ -406,6 +406,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import DOMPurify from 'dompurify'
import { rpcClient } from '@/api/rpc-client'
import { useAppStore } from '@/stores/app'
import QuickActionsCard from './server/QuickActionsCard.vue'
@ -499,6 +500,11 @@ const showAddDeviceModal = ref(false)
const newPeerName = ref('')
const creatingPeer = ref(false)
const peerQrData = ref<{ qr_svg: string; config: string; peer_ip: string } | null>(null)
// Sanitize like TwoFactorSection's TOTP QR the SVG is backend-generated,
// but v-html without a sanitizer is one compromised RPC away from XSS.
const sanitizedPeerQrSvg = computed(() =>
DOMPurify.sanitize(peerQrData.value?.qr_svg ?? '', { USE_PROFILES: { svg: true } }),
)
const peerError = ref('')
const copiedConfig = ref(false)
const vpnPeers = ref<{ name: string; ip: string; type?: string; npub?: string }[]>([])

View File

@ -57,7 +57,10 @@ export async function fetchAppCatalog(): Promise<AppCatalog | null> {
// Cache in localStorage for offline fallback
try { localStorage.setItem('archy_catalog', JSON.stringify(data)) } catch {}
return data
} catch { continue }
} catch (e) {
console.warn(`[catalog] fetch failed for ${url}:`, e)
continue
}
}
// Try localStorage cache as final fallback
@ -68,8 +71,11 @@ export async function fetchAppCatalog(): Promise<AppCatalog | null> {
catalogFetchedAt = Date.now() - CATALOG_TTL + 5 * 60 * 1000 // re-check in 5 min
return cachedCatalog
}
} catch {}
} catch (e) {
console.warn('[catalog] localStorage fallback unreadable:', e)
}
console.warn('[catalog] all sources failed — using hardcoded app list')
return null
}