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:
parent
4c75bb3d38
commit
2f20ba8148
@ -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 }
|
||||
})
|
||||
|
||||
@ -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 }[]>([])
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user