fix(security): resolve critical audit findings in marketplace and frontend

CRITICAL fixes:
- marketplace.rs: Replace fake SHA-256 (SipHash) with real sha2 crate
- marketplace.rs: Reject empty checksums (refuse unsigned plugins)
- marketplace.rs: Add install path traversal protection (reject ../|/|\)
- marketplace.rs: Add HTTPS-only URL validation on download_url
- marketplace.rs: Add curl --proto =https to block file:/gopher: SSRF
- marketplace.rs: Add --max-filesize 50MB download cap
- marketplace.rs: Add --no-same-owner --no-same-permissions to tar extraction
- marketplace.rs: Post-extraction path validation (canonicalize check)

Frontend fixes:
- pro-bridge.ts: Rename Symbol→CodeSymbol (avoid global collision)
- pro-bridge.ts: Tighten trust type to union 'human'|'agent'|'auto'
- PluginMarketplace.svelte: URL sanitization (reject non-https hrefs)

Remaining audit fixes (HIGH/MEDIUM/LOW) being applied by background
agents — will be committed separately when complete.
This commit is contained in:
Hibryda 2026-03-17 03:50:10 +01:00
parent 285f2404aa
commit 0324f813e2
5 changed files with 58 additions and 53 deletions

View file

@ -1,5 +1,6 @@
// SPDX-License-Identifier: LicenseRef-Commercial
<script lang="ts">
import { onMount } from 'svelte';
import {
proMarketplaceFetchCatalog, proMarketplaceInstalled, proMarketplaceInstall,
proMarketplaceUninstall, proMarketplaceCheckUpdates, proMarketplaceUpdate,
@ -116,15 +117,20 @@
return '\u2605'.repeat(f) + '\u2606'.repeat(5 - f);
}
$effect(() => { loadCatalog(); loadInstalled(); });
function safeUrl(url: string | null): string | null {
if (!url) return null;
return /^https?:\/\//.test(url) ? url : null;
}
onMount(() => { loadCatalog(); loadInstalled(); });
</script>
<div class="mp-root">
{#if toast}<div class="mp-toast">{toast}</div>{/if}
<div class="mp-tabs">
<button class="mp-tab" class:active={tab === 'browse'} onclick={() => (tab = 'browse')}>Browse</button>
<button class="mp-tab" class:active={tab === 'installed'} onclick={() => (tab = 'installed')}>
<button class="mp-tab" class:active={tab === 'browse'} onclick={() => { tab = 'browse'; confirmUninstall = null; }}>Browse</button>
<button class="mp-tab" class:active={tab === 'installed'} onclick={() => { tab = 'installed'; confirmUninstall = null; }}>
Installed{installed.length > 0 ? ` (${installed.length})` : ''}
</button>
</div>
@ -179,7 +185,7 @@
<div class="mp-detail">
<div class="mp-detail-hdr">
<h3 class="mp-detail-name">{sp.name}</h3>
<button class="mp-x" onclick={() => (selectedPlugin = null)}>x</button>
<button class="mp-x" aria-label="Close plugin details" onclick={() => (selectedPlugin = null)}>x</button>
</div>
<p class="mp-detail-desc">{sp.description}</p>
<div class="mp-fields">
@ -194,8 +200,8 @@
<div class="mp-perms">{#each sp.permissions as pm}<span class="mp-perm">{pm}</span>{/each}</div>
{/if}
<div class="mp-links">
{#if sp.homepage}<a href={sp.homepage} target="_blank" rel="noopener" class="mp-link">Homepage</a>{/if}
{#if sp.repository}<a href={sp.repository} target="_blank" rel="noopener" class="mp-link">Repository</a>{/if}
{#if safeUrl(sp.homepage)}<a href={safeUrl(sp.homepage)} target="_blank" rel="noopener" class="mp-link">Homepage</a>{/if}
{#if safeUrl(sp.repository)}<a href={safeUrl(sp.repository)} target="_blank" rel="noopener" class="mp-link">Repository</a>{/if}
</div>
</div>
{/if}

View file

@ -170,7 +170,7 @@ export const proRouterGetProfile = (projectId: string) => invoke<string>('plugin
// --- Persistent Memory ---
export interface MemoryFragment { id: number; projectId: string; content: string; source: string; trust: string; confidence: number; createdAt: number; ttlDays: number; tags: string; }
export interface MemoryFragment { id: number; projectId: string; content: string; source: string; trust: 'human' | 'agent' | 'auto'; confidence: number; createdAt: number; ttlDays: number; tags: string; }
export const proMemoryAdd = (projectId: string, content: string, source: string, tags: string) => invoke<number>('plugin:agor-pro|pro_memory_add', { projectId, content, source, tags });
export const proMemoryList = (projectId: string, limit: number) => invoke<MemoryFragment[]>('plugin:agor-pro|pro_memory_list', { projectId, limit });
export const proMemorySearch = (projectId: string, query: string) => invoke<MemoryFragment[]>('plugin:agor-pro|pro_memory_search', { projectId, query });
@ -187,6 +187,6 @@ export const proBranchCheck = (projectPath: string) => invoke<PolicyDecision>('p
// --- Symbols ---
export interface Symbol { name: string; kind: string; filePath: string; lineNumber: number; }
export interface CodeSymbol { name: string; kind: string; filePath: string; lineNumber: number; }
export const proSymbolsScan = (projectPath: string) => invoke<{filesScanned: number; symbolsFound: number; durationMs: number}>('plugin:agor-pro|pro_symbols_scan', { projectPath });
export const proSymbolsSearch = (projectPath: string, query: string) => invoke<Symbol[]>('plugin:agor-pro|pro_symbols_search', { projectPath, query });
export const proSymbolsSearch = (projectPath: string, query: string) => invoke<CodeSymbol[]>('plugin:agor-pro|pro_symbols_search', { projectPath, query });