Skip to content

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) — absolute http(s) only; null otherwise. Used for navigation targets and media src.
  • safeImageUrl(value)http(s) or base64 image data: URLs; used for <img src>.

Known sinks (all must use the helpers):

  • evidence items on DocketDetail and the Evidence workspace;
  • the fullscreen evidence lightbox (sanitization was added there in the security pass);
  • DiscordProfileCard / DiscordProfileModal avatar and banner images;
  • any window.open with 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 no dangerouslySetInnerHTML for 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") in AppLayout; page-level controls gate on the same flags (e.g. admin tabs render only when the corresponding capability is set).
  • SensitiveField masks 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/me and 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 confirmBeforeClose preference.

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.