Skip to content

Harden checkpoint store against path traversal and resource exhaustion#18

Merged
antonpk1 merged 1 commit into
excalidraw:mainfrom
debu-sinha:fix/checkpoint-security-hardening
Feb 12, 2026
Merged

Harden checkpoint store against path traversal and resource exhaustion#18
antonpk1 merged 1 commit into
excalidraw:mainfrom
debu-sinha:fix/checkpoint-security-hardening

Conversation

@debu-sinha

Copy link
Copy Markdown
Contributor

What this fixes

The checkpoint ID flows from client input directly into filesystem paths via path.join(this.dir, id + ".json") without sanitization. A crafted checkpoint ID containing path traversal sequences (e.g., ../../etc/cron.d/evil) could write arbitrary JSON files outside the intended checkpoint directory.

While save_checkpoint and read_checkpoint are scoped to widget visibility (["app"]), the input still reaches FileCheckpointStore unsanitized. In HTTP/Streamable transport mode, this surface is reachable without additional authentication.

Additionally, there are no bounds on input size or checkpoint count, which could lead to disk/memory exhaustion.

I wrote about these patterns more broadly in The MCP Ecosystem Has a Security Problem -- this PR addresses the specific instances found here.

Changes

Path traversal prevention (checkpoint-store.ts):

  • Added validateCheckpointId() that rejects any ID not matching [a-zA-Z0-9_-] with a 64-char length cap
  • Added resolved path verification (path.resolve prefix check) as defense-in-depth

Resource exhaustion limits:

  • 5 MB cap on serialized checkpoint data (applies to all three store backends)
  • 5 MB cap on elements, json, and data tool inputs in server.ts
  • FileCheckpointStore now prunes oldest checkpoints beyond 100 entries (matching the TTL that RedisCheckpointStore already has)
  • MemoryCheckpointStore evicts oldest entry when exceeding limit

No breaking changes -- server-generated checkpoint IDs are already hex UUIDs (crypto.randomUUID().replace(/-/g, "").slice(0, 18)) which pass the new validation. The size limits are generous enough for normal usage.

Testing

  • npx tsc --noEmit passes with no type errors
  • Verified that server-generated checkpoint IDs (18-char hex strings) pass validation
  • Verified that traversal payloads like ../../etc/passwd and ../../../tmp/evil are rejected

The checkpoint ID parameter flows from client input to filesystem paths
without validation. A crafted ID like "../../etc/cron.d/evil" would write
files outside the checkpoint directory via path.join.

This adds three layers of defense:

- Checkpoint ID validation: reject anything that isn't alphanumeric,
  hyphens, or underscores (blocks path traversal characters)
- Resolved path verification: confirm the final path stays within the
  checkpoint directory even after symlink resolution
- Input size limits: cap elements/data/json inputs at 5 MB to prevent
  memory and disk exhaustion
- Checkpoint pruning: FileCheckpointStore now evicts oldest entries
  beyond 100 to prevent unbounded disk growth, matching the TTL that
  RedisCheckpointStore already has

Signed-off-by: debu-sinha <debusinha2009@gmail.com>
@vercel

vercel Bot commented Feb 12, 2026

Copy link
Copy Markdown

@debu-sinha is attempting to deploy a commit to the Excalidraw Team on Vercel.

A member of the Team first needs to authorize it.

@antonpk1 antonpk1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super helpful, thanks!

@antonpk1 antonpk1 merged commit b384815 into excalidraw:main Feb 12, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants