diff --git a/README.md b/README.md index c0a8a294..f0505470 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ **This is a fork of [Excalidraw](https://excalidraw.com).** All credit for the original project goes to the Excalidraw team and contributors. The upstream project lives at [github.com/excalidraw/excalidraw](https://github.com/excalidraw/excalidraw). I can't recommend enough if you find use in this to [sponsor Excalidraw](https://opencollective.com/excalidraw), they did all the hard parts. ## What was added -- Atprotocal support. Login into your Atmosphere account and save your drawings to your PDS making them public as well as a cloud sync + +- Atprotocal support. Login into your Atmosphere account and save your drawings to your PDS making them public as well. - Revisions to help you keep what you care about - Collaborative editing via [iroh](https://iroh.computer). When you share a drawing for collaborative editing it is end to end encrypted as well as peer to peer. diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx index 0d50006c..065d5b78 100644 --- a/excalidraw-app/App.tsx +++ b/excalidraw-app/App.tsx @@ -141,6 +141,7 @@ import { import { getCurrentSceneKey } from "./data/sceneIdentity"; import { mergedSceneIdentity } from "./data/loadConflict"; import { preserveTheme } from "./data/preserveTheme"; +import { sanitizeUntrustedElements } from "./data/sanitizeUntrusted"; import { FindDrawingsDialog, findDrawingsDialogOpenAtom, @@ -475,7 +476,15 @@ const initializeScene = async (opts: { !scene.elements.length || (await openConfirmModal(shareableLinkConfirmDialog)) ) { - return { scene: data, isExternalScene: true }; + return { + // arbitrary URL, and the confirm modal above is skipped entirely on + // an empty canvas + scene: { + ...data, + elements: sanitizeUntrustedElements(data.elements), + }, + isExternalScene: true, + }; } } catch (error: any) { return { diff --git a/excalidraw-app/collab/Collab.tsx b/excalidraw-app/collab/Collab.tsx index 3dc7d00c..17fdc7b1 100644 --- a/excalidraw-app/collab/Collab.tsx +++ b/excalidraw-app/collab/Collab.tsx @@ -88,6 +88,7 @@ import { importUsernameFromLocalStorage, saveUsernameToLocalStorage, } from "../data/localStorage"; +import { sanitizeUntrustedElements } from "../data/sanitizeUntrusted"; import { resetBrowserStateVersions } from "../data/tabSync"; import { collabErrorIndicatorAtom } from "./CollabError"; @@ -1149,6 +1150,10 @@ class Collab extends PureComponent { const existingElements = this.getSceneElementsIncludingDeleted(); + // anyone holding the room ticket can broadcast arbitrary bytes, so peer + // elements are untrusted input + remoteElements = sanitizeUntrustedElements(remoteElements); + // NOTE ideally we restore _after_ reconciliation but we can't do that // as we'd regenerate even elements such as appState.newElement which would // break the state diff --git a/excalidraw-app/data/atproto/publicRead.ts b/excalidraw-app/data/atproto/publicRead.ts index a54f3006..3eb19e80 100644 --- a/excalidraw-app/data/atproto/publicRead.ts +++ b/excalidraw-app/data/atproto/publicRead.ts @@ -7,6 +7,8 @@ import { loadFromBlob } from "@excalidraw/excalidraw/data/blob"; import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore"; +import { sanitizeUntrustedElements } from "../sanitizeUntrusted"; + import { maybeDecompressSceneBytes } from "./compression"; import { app, com } from "./lexicons"; @@ -138,7 +140,9 @@ export const fetchPublicScene = async ( null, ); return { - scene, + // the blob bytes are whatever the PDS chose to serve — the lexicon only + // validates the record envelope, and there's no CID check on the body + scene: { ...scene, elements: sanitizeUntrustedElements(scene.elements) }, name: value.name, createdAt: value.createdAt, updatedAt: value.updatedAt, diff --git a/excalidraw-app/data/atproto/scenes.ts b/excalidraw-app/data/atproto/scenes.ts index 79a968b2..1fc04a84 100644 --- a/excalidraw-app/data/atproto/scenes.ts +++ b/excalidraw-app/data/atproto/scenes.ts @@ -11,6 +11,7 @@ import type { } from "@excalidraw/excalidraw/data/reconcile"; import type { AppState, BinaryFiles } from "@excalidraw/excalidraw/types"; +import { sanitizeUntrustedElements } from "../sanitizeUntrusted"; import { generateThumbnailBlob } from "../thumbnail"; import { prepareSceneUpload } from "./blobCid"; @@ -141,7 +142,9 @@ const fetchRemoteScene = async ( return { cid: recordRes.data.cid, - elements: restored.elements, + // own repo, but still PDS-served bytes behind an unvalidated `as` cast and + // no CID check — same boundary as restoreCloudCheckpoint + elements: sanitizeUntrustedElements(restored.elements), }; }; diff --git a/excalidraw-app/data/restoreRevision.ts b/excalidraw-app/data/restoreRevision.ts index b3752a08..c9347da1 100644 --- a/excalidraw-app/data/restoreRevision.ts +++ b/excalidraw-app/data/restoreRevision.ts @@ -19,6 +19,7 @@ import type { import { fetchCheckpointScene } from "./atproto/revisionsPds"; import { preserveTheme } from "./preserveTheme"; import { getRevision, putRevision } from "./revisions"; +import { sanitizeUntrustedElements } from "./sanitizeUntrusted"; import type { ActiveScene } from "./atproto/activeScene"; import type { CollabAPI } from "../collab/Collab"; @@ -141,9 +142,15 @@ export const restoreCloudCheckpoint = async ({ currentFiles, ); - // 2. fetch and rehydrate the checkpoint blob from the PDS + // 2. fetch and rehydrate the checkpoint blob from the PDS. Own repo, but the + // blobCid comes from a listCheckpoints read that isn't lexicon-validated, and + // the PDS body isn't CID-checked — so treat the elements as untrusted. const blob = await fetchCheckpointScene(agent, blobCid); - const restored = await loadFromBlob(blob, null, null); + const loaded = await loadFromBlob(blob, null, null); + const restored = { + ...loaded, + elements: sanitizeUntrustedElements(loaded.elements), + }; // 3. swap it onto the canvas — mid-collab, the swap must be broadcast as a // full replacement (tombstoned old elements) or peers never see it diff --git a/excalidraw-app/data/sanitizeUntrusted.ts b/excalidraw-app/data/sanitizeUntrusted.ts new file mode 100644 index 00000000..6f38e023 --- /dev/null +++ b/excalidraw-app/data/sanitizeUntrusted.ts @@ -0,0 +1,50 @@ +import type { ExcalidrawElement } from "@excalidraw/element/types"; + +/** + * `customData` keys that are inert as stored data but become executable once + * the editor renders them. + * + * `generationData` is upstream's wireframe-to-code payload. When its status is + * "done", `App.renderEmbeddables` feeds `.html` verbatim into an `