Files
clienthub/src/components/public/proposal/sections/ScopeSection.tsx
T
simone e2bd1d95ed fix(security): audit completo — secondo gate admin, hardening slug, XSS, CSP/HSTS, update CVE
Audit di sicurezza su tutta l'app. Report in .planning/SECURITY-SCAN.md (codice),
.planning/SECURITY-AUDIT-INFRA.md (dipendenze/segreti/deploy) e piano in
.planning/SECURITY-REMEDIATION-PLAN.md.

CRITICO — l'autorizzazione admin era un unico punto di rottura: nessuna delle 21
pagine /admin controllava la sessione e admin/layout.tsx renderizzava comunque i
figli quando mancava. L'unico guard era proxy.ts, su un Next.js affetto da
GHSA-6gpp-xcg3-4w24 (proxy bypass). Ora il layout è un secondo gate indipendente;
proxy.ts marca il path con un token derivato da NEXTAUTH_SECRET, così il gate non
è aggirabile forgiando header e fallisce chiuso se il proxy non gira.

ALTO — gli slug cliente avevano 4 caratteri casuali da Math.random() (~20 bit,
1.7M tentativi) e risolvono prima del token: ora 12 caratteri via nanoid
(CSPRNG, ~62 bit). Aggiunto rate limit al ramo /client/, che ne era privo.

ALTO — src/lib/quote-actions.ts esponeva due server action pubbliche senza
autenticazione, una delle quali scriveva su DB. Codice morto, zero chiamanti:
rimosso.

MEDIO — i quattro dangerouslySetInnerHTML nelle sezioni proposta rendevano output
AI come HTML grezzo su pagina pubblica, alimentato da transcript di terzi. Sostituiti
con RichText (whitelist di emphasis, nessun HTML al DOM). I transcript ora sono
recintati in tag che il system prompt dichiara essere dati, non istruzioni.

Inoltre: next 16.2.6 -> 16.2.12 e next-auth 4.24.14 -> 4.24.15 (chiude 9 CVE Next
piu GHSA-xmf8-cvqr-rfgj su getToken, raggiungibile dal proxy); HSTS e CSP;
potatura della Map di rate-limit.ts, che cresceva senza limite; espunta la password
Postgres di produzione dai due 07-01-SUMMARY.md.

Verificato: tsc pulito, build OK, smoke test su login/redirect/header forgiati.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 23:35:46 +02:00

41 lines
2.0 KiB
TypeScript

import type { ProposalContent } from "@/lib/proposal/schema";
import { CheckCircle2 } from "lucide-react";
import { RichText } from "@/components/public/proposal/RichText";
type Props = { scope: ProposalContent["scope"] };
export function ScopeSection({ scope }: Props) {
return (
<div className="h-full flex items-center px-16 py-24">
<div className="grid grid-cols-2 gap-12 w-full">
{/* Scope */}
<div className="border border-primary/20 bg-primary/5 rounded-xl p-8 space-y-4">
<div className="w-10 h-10 rounded-full border border-primary flex items-center justify-center">
<div className="w-5 h-5 rounded-full bg-primary/30" />
</div>
<p className="text-xs font-mono tracking-widest text-primary uppercase">Scope · Capitolo del lavoro</p>
<h3 className="text-xl font-medium text-foreground">{scope.scopeTitle}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{scope.scopeBody}</p>
</div>
{/* Obiettivi */}
<div className="border border-border bg-muted/30 rounded-xl p-8 space-y-4">
<div className="w-10 h-10 rounded-full border border-border flex items-center justify-center">
<div className="w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-b-[10px] border-b-foreground" />
</div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Obiettivi · Punti d&apos;arrivo</p>
<h3 className="text-xl font-medium text-foreground">Quattro risultati concreti, misurabili al termine.</h3>
<ul className="space-y-2">
{scope.objectives.map((obj, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-foreground">
<CheckCircle2 size={14} className="text-primary mt-0.5 shrink-0" />
<RichText>{obj}</RichText>
</li>
))}
</ul>
</div>
</div>
</div>
);
}