Appearance
Security-Sensitive UI
The frontend is not a security boundary — the API re-checks everything — but several UI surfaces are security-sensitive in the sense that a mistake there creates vulnerabilities or leaks. This page documents the rules.
URL sanitization
Every string from the API that will be placed into src, href, or a navigation API must pass through src/lib/urlSafety.ts:
safeHttpUrl(value)— absolutehttp(s)only;nullotherwise. Used for navigation targets and mediasrc.safeImageUrl(value)—http(s)or base64 imagedata:URLs; used for<img src>.
Known sinks (all must use the helpers):
- evidence items on
DocketDetailand the Evidence workspace; - the fullscreen evidence lightbox (sanitization was added there in the security pass);
DiscordProfileCard/DiscordProfileModalavatar and banner images;- any
window.openwith server-provided content.
Rule: a URL that fails sanitization renders as a fallback — never as a raw attribute value. Adding a new component that renders media must follow this pattern (see Media / URL Safety).
Media rendering
- Images load as normal
<img>after scheme validation; there is no raw HTML embedding of server content and nodangerouslySetInnerHTMLfor API data. - React's text escaping covers interpolation; URL sinks are the only place where validation matters in practice.
- The Settings avatar upload uses base64 image data URLs — the explicit exception implemented in
safeImageUrl.
Permission-aware controls
- Navigation is filtered by
can(permission)/canAdmin("admin_panel")inAppLayout; page-level controls gate on the same flags (e.g. admin tabs render only when the corresponding capability is set). SensitiveFieldmasks sensitive identifiers (agent ID, clearance ID, agreement ID) for everyone; clearance-4+ users can reveal after double-click confirmation.- Supervisor-only content (supervisor queues, supervisor dockets) is hidden for non-supervisors.
- These flags come from
/auth/meand are advisory: they mirror the API's checks but never replace them. If the API rejects an action, the UI must surface the error and re-fetch, because the agent's capabilities may have changed server-side.
Destructive-action confirmation
High-impact mutations require explicit confirmation with a reason where the API demands one:
AdminConfirmDialog— admin record corrections and destructive admin actions with a required reason field.- Docket deletion and punishment revocation collect a reason in a modal before the API call (the API rejects missing reasons).
- Page-local confirm flows for close/end actions honor the
confirmBeforeClosepreference.
Handling of server-provided content
- Report text, notes, evidence descriptions, and audit entries are rendered as plain text (React-escaped). Long content is truncated in list views.
- Discord profile data (names, roles, nicknames) is display data only — identity keys (
discord_id) are never treated as display names. - Training content (scenario definitions, injections, notes) is rendered as text; scenario definitions are data, not code — the engine evaluates conditions structurally and never executes strings.