# Security Audit
## Summary
5chan's React frontend has a generally defensive posture for an imageboard that renders untrusted peer content: zero `dangerouslySetInnerHTML` usages (prior scan confirmed), custom markdown tokenizer that leans on React's JSX escaping, `target="_blank"` links uniformly paired with `rel="noopener noreferrer"`, and URL parsing routed through the browser `URL` constructor. The highest-impact issues are a severely broken crypto RNG polyfill that silently downgrades `crypto.getRandomValues` to `Math.random`, third-party embed scripts loaded into `about:srcdoc` iframes that inherit the 5chan origin without Subresource Integrity, and the complete absence of a `Content-Security-Policy` header — so any future XSS has no defense-in-depth to contain it. Several medium issues stem from peer-controlled URLs flowing into ` `, ``, ``, ``, and iframe embedders without origin validation, which is acceptable for many decentralized imageboard designs but warrants an explicit allow-list or protocol filter.
## Threat Surface Overview
| Trust boundary | Where it enters the client | Key files |
|---|---|---|
| Peer post content (title, content, link, reason) | Subplebbit/community feeds via `bitsocial-react-hooks` | `src/components/markdown/markdown.tsx`, `src/components/comment-content/comment-content.tsx`, `src/components/comment-media/comment-media.tsx`, `src/views/archive/archive.tsx` |
| Peer-supplied media URLs | Comment `link` field | `src/lib/utils/media-utils.ts`, `src/components/comment-media/comment-media.tsx`, `src/components/catalog-row/catalog-row.tsx`, `src/components/embed/embed.tsx` |
| External iframe challenges (captcha) | `challenge` field of type `url/iframe` | `src/components/challenge-modal/challenge-modal.tsx` |
| External webpage thumbnails (og:image/first ` `) | Community-served link preview fetched via `fetch()`/`CapacitorHttp` | `src/lib/utils/media-utils.ts` (`fetchWebpageThumbnail`) |
| GitHub release manifest | `api.github.com` release JSON for auto-update | `src/lib/app-update.ts`, `src/lib/app-update-config.ts` |
| GitHub directories list | `raw.githubusercontent.com` JSON | `src/hooks/use-directories.ts` |
| Account data / private keys | bitsocial-react-hooks account store; editor reads/writes JSON including `signer` | `src/views/account-data-editor/account-data-editor.tsx`, `src/components/settings-modal/account-settings/account-settings.tsx`, `src/lib/utils/account-editor-utils.ts` |
| Local preferences | `localStorage` keys (filters, subscriptions, UI state) | `src/stores/use-*-store.ts`, `src/hooks/use-directories.ts` |
| URL query/hash routing | Location search/hash parsed via `URLSearchParams`/`is5chanLink` | `src/lib/utils/url-utils.ts`, `src/components/catalog-search/catalog-search.tsx`, `index.html` |
| User regex hide filters | User-typed patterns compiled via `new RegExp` | `src/lib/utils/pattern-utils.ts` |
| Native bridges | `window.electronApi.*` (copy clipboard, upload automation, installer) | `src/globals.d.ts`, `src/hooks/use-file-upload.ts`, `src/lib/app-update.ts`, `src/lib/utils/clipboard-utils.ts` |
## Findings
### Critical
- **src/polyfills.js:20-29** — `window.crypto.getRandomValues` is replaced with a `Math.random()`-backed fallback when `window.crypto` is undefined. `Math.random` is a non-cryptographic PRNG (typically xorshift/PCG seeded at startup, fully predictable). Anything that later calls `crypto.getRandomValues` under this polyfill — libsodium, noble-curves/ed25519, IPFS/libp2p key generation, nonces, session IDs inside bundled deps — silently produces guessable output. In modern browsers the branch shouldn't trip (crypto always exists), but the guard also short-circuits *any* environment where `crypto` exists but `getRandomValues` is missing (older SSR shims, some Electron preload corner cases), so the defensive posture is broken rather than safe-by-default. The severity is magnified because a decentralized imageboard derives account keypairs, signs publications, and derives author addresses from this primitive layer. *Fix:* remove the Math.random fallback entirely. If a host truly lacks `crypto.getRandomValues`, `throw` so callers bail; do not fabricate randomness. At minimum, guard the replacement behind a build-time flag so it never ships to production.
### High
- **src/components/embed/embed.tsx:113-315** — Third-party embed scripts (`platform.twitter.com/widgets.js`, `embed.reddit.com/widgets.js`, `www.tiktok.com/embed.js`, `//www.instagram.com/embed.js`) are injected via `srcDoc` iframes with no `sandbox` attribute and no Subresource Integrity hash. An `about:srcdoc` iframe inherits the embedding document's origin, so any script it loads executes *as 5chan.app* and can reach `window.parent.document`. A supply-chain compromise of any of those four CDNs (or a routed MITM on a user without HSTS) directly yields full XSS of the 5chan app, including read/write of IndexedDB account keys. This is amplified by the `//www.instagram.com/embed.js` scheme-relative URL, which downgrades to `http://` if the top-level page is ever served non-TLS (Electron `file://`, local dev). *Fix:* (a) add `sandbox="allow-scripts allow-popups allow-same-origin"` carefully chosen per embed type so cross-origin script compromise cannot reach parent; ideally omit `allow-same-origin` and rely on the iframe being a true opaque origin; (b) pin `src="https://..."` absolute for Instagram; (c) consider inlining the embed via the official oEmbed server-side rendering path or proxying through a CSP-constrained iframe host (e.g. a sandbox subdomain).
- **index.html (entire file) + vercel.json:36-45** — No `Content-Security-Policy` header is emitted and no ` ` exists. Given that the primary threat model is untrusted peer content and third-party scripts load into the origin via srcdoc iframes, a CSP is the most impactful missing control. A single XSS bypass (today, future regression, or supply-chain) would have full DOM access. *Fix:* add a strict CSP via Vercel header: `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; frame-src https:; img-src https: data: blob:; media-src https: blob:; connect-src 'self' https: wss:; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`. Tighten over time (drop `'unsafe-inline'` for styles once CSS-modules replaces any inline styles). Mirror with `Strict-Transport-Security: max-age=31536000; includeSubDomains` and, since the app is hash-routed, consider `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Resource-Policy: same-origin` to block spectre-style side-channels.
- **src/components/comment-media/comment-media.tsx:74, 89, 93, 108, 124, 138, 218, 235, 238, 349, 388; src/components/catalog-row/catalog-row.tsx:84-101** — Peer-supplied `url` / `thumbnail` / `gifFrameUrl` are rendered directly as ` `, ``, and `` with no scheme or host allow-list. `getLinkMediaInfo` only validates that `new URL(link)` succeeds (`src/lib/utils/url-utils.ts:13-20`), so `javascript:foo` parses successfully and flows through. React auto-strips `javascript:` for `href` and `src` on ``/`