CodeVetter · Repo Unpack · Risk map
excalidraw/excalidraw
The serious boundary is collaboration storage: Firestore and Storage rules allow public get and write, and confidentiality is entirely the AES-GCM room key that also sits in the URL hash. The editor class in App.tsx is a single very large state owner, so small input bugs have a wide blast radius. Several legacy paths (zero-IV decrypt, CRA service worker, index-node canvas script) are still in tree. Collaboration, Firebase, and the AI backends are the least tested production paths.
- firebase-project/firestore.rules allows get and write on every document if true, and explicitly disables list so collections cannot be enumerated. storage.rules allows get and write with no auth on /{files}/rooms/{room}/{file} and /{files}/shareLinks/{shareLink}/{file}. A comment in the Firestore file says list must never be enabled or anyone could delete others' drawings. Anyone who knows or guesses a room id can overwrite the ciphertext; they cannot read it without the key.
- The room key and the share-link key are placed in the URL hash on purpose so they are not sent to the server (getCollaborationLink and exportToBackend comments). Sentry's beforeSend strips hash from event.request.url for the same reason. The key still leaks via screenshots, browser history, and Referrer-Policy origin (not no-referrer) if a navigation includes a hash the browser keeps locally.
- Encryption is AES-GCM with a 128-bit key (A128GCM), 12-byte random IV, and Web Crypto. The legacy share decoder still accepts a 12-byte zero IV if the length-prefixed IV fails, which is a weaker historical format that remains reachable through importFromBackend.
- initializeScene will fetch an arbitrary decoded URL when location.hash matches /^#url=(.*)$/, then parse the body with loadFromBlob. That is an open client-side fetch of attacker-controlled content, followed by scene restore if the user confirms or the canvas is empty.
- isExcalidrawPlusSignedUser is document.cookie.includes('excplus-auth'). It detects cookie presence, not a verified session. ExcalidrawPlusIframeExport posts to VITE_APP_PLUS_APP and attaches VITE_APP_PLUS_EXPORT_PUBLIC_KEY.
- Sentry DSN https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260 is hardcoded. It is disabled when VITE_APP_DISABLE_SENTRY=true (the Docker build sets that) or when the hostname is not in SentryEnvHostnameMap. beforeSend fabricates an exception from callsites() for console errors, and captureConsoleIntegration records level error.
- vercel.json sets Feature-Policy to * on all routes, which is a permissive feature policy, alongside nosniff and Referrer-Policy origin. Font woff2 routes under Virgil, Cascadia, and Assistant-Regular allow Access-Control-Allow-Origin *.
- packages/excalidraw/index-node.ts is a Node script that require()s fs and the canvas package, registers Virgil and Cascadia, calls exportToCanvas on three hardcoded elements, and writes test.png. It is not the browser entry (package.json main is dist/prod/index.js) and it shells into the filesystem.
- public/service-worker.js is a leftover Create-React-App worker whose activate handler unregisters itself and reloads every client. The comment says Vite now uses sw.js from vite-plugin-pwa and this file should be removed once old clients are gone. It still ships in public/.
- QR rendering uses dangerouslySetInnerHTML={{ __html: svgData }} in excalidraw-app/share/QRCode.tsx. The SVG is produced locally by the uqr dependency, but the sink is still raw HTML injection if that string is ever attacker-controlled.
- Release and doc scripts shell out: scripts/release.js and scripts/build-version.js use child_process, scripts/buildDocs.js runs git diff, scripts/updateChangelog.js execs git. These are maintainer entrypoints, not the browser app, but they take CLI arguments such as --tag and --version.
- The editor App class is the main blast-radius file: constructor, pointer handling, keyboard, export, and unmount all live there, with methods past line 11000 (actionFinalize calls around 10134–11687). Store.scheduleCapture is commented as suspicious because it is called from many sites. A mistaken CaptureUpdateAction.IMMEDIATELY on a remote update would pollute undo; NEVER on a local edit would drop it.
- Library installs only allow excalidraw.com and the excalidraw-libraries GitHub raw host, but collab image and scene writes are not similarly origin-locked. Firebase config JSON parse failure only console.warns and continues with {}. A missing or malformed VITE_APP_FIREBASE_CONFIG therefore fails later, at first collab save, rather than at startup.
- window.h in dev and test exposes setState and the store. Combined with StrictMode double-mount, calling the imperative API after unmount throws a specific destroyed-API error. Hosts that cache the first onExcalidrawAPI value across StrictMode will hit that.