Compare commits

..

49 Commits

Author SHA1 Message Date
Zimeng Xiong 4bc66ab014 MVP passwords 2025-11-28 10:19:44 -08:00
Zimeng Xiong 971046d568 Update README 2025-11-24 15:04:52 -08:00
Zimeng Xiong 602350d2e6 Merge pull request #9 from ZimengXiong/pre-release
v0.1.6 Add export button, store library in database
2025-11-24 15:01:02 -08:00
Zimeng Xiong f20d48fea2 fix migration issues 2025-11-24 14:53:17 -08:00
Zimeng Xiong c53dc010de Merge branch '8-export-drawing' into pre-release 2025-11-24 14:43:58 -08:00
Zimeng Xiong 03e778a06f add export functionality via exportUtils 2025-11-24 14:39:38 -08:00
Zimeng Xiong fa73708d97 allow importing of libraries via URL, update db schema 2025-11-24 14:32:48 -08:00
Zimeng Xiong ee8204532d Update README.md 2025-11-23 10:23:24 -08:00
Zimeng Xiong a347403a26 Fix caution message formatting in README 2025-11-23 10:15:51 -08:00
Zimeng Xiong 8becfd87bb Merge pull request #6 from ZimengXiong/pre-release
v0.1.5 Fix security issues.
2025-11-23 10:08:42 -08:00
Zimeng Xiong 1b78597649 Merge branch 'main' into pre-release 2025-11-23 10:06:08 -08:00
Zimeng Xiong d93b6493c1 fix database import in docker 2025-11-23 09:40:00 -08:00
Zimeng Xiong d581eb3e88 fix database import, allow sqlite and db format 2025-11-23 09:22:01 -08:00
Zimeng Xiong 4728ef151c release notes 2025-11-23 09:12:36 -08:00
Zimeng Xiong eb5f54a6d0 unify version numbering 2025-11-23 08:53:36 -08:00
Zimeng Xiong c502f1c0bd add version card to settings, branch push protection 2025-11-23 08:35:36 -08:00
Zimeng Xiong 8f9ac1f9c0 add dev tag to pre release dockerhub images 2025-11-23 08:03:48 -08:00
Zimeng Xiong 0787989496 add version managment script 2025-11-23 08:02:00 -08:00
Zimeng Xiong 9bc25a3dc2 update README, release notes 2025-11-23 07:43:14 -08:00
Zimeng Xiong 3cc3fd18f4 add prerelease docker script 2025-11-23 07:30:20 -08:00
Zimeng Xiong 997fa4af03 add prisma cli to dependencies, make zod checks more permissive 2025-11-23 07:08:41 -08:00
Zimeng Xiong b864e82318 Merge branch '1-413-request-entity-too-large' into pre-release 2025-11-22 22:50:40 -08:00
Zimeng Xiong 2f22be2bd7 Merge branch 'fix-CPU-blocking' into pre-release 2025-11-22 22:48:51 -08:00
Zimeng Xiong fcfb850168 Merge branch 'fix-DoS-event-blocking' into pre-release 2025-11-22 22:44:27 -08:00
Zimeng Xiong 4a224c1f92 Merge branch 'fix-rce-via-upload' into pre-release 2025-11-22 22:43:47 -08:00
Zimeng Xiong d1d17e1288 Merge branch 'fix-xss-root-execution' into pre-release 2025-11-22 22:43:31 -08:00
Zimeng Xiong 9055661b51 make async database integrity check 2025-11-22 21:59:18 -08:00
Zimeng Xiong d25a32cdd3 Fix license badge URL in README.md 2025-11-22 21:56:14 -08:00
Zimeng Xiong 8d65404514 Fix license badge URL in README.md 2025-11-22 21:56:14 -08:00
Zimeng Xiong 1b6c32d773 Merge pull request #3 from ZimengXiong/ZimengXiong-patch-1
Create LICENSE
2025-11-22 21:54:46 -08:00
Zimeng Xiong 352bcfca29 Merge pull request #3 from ZimengXiong/ZimengXiong-patch-1
Create LICENSE
2025-11-22 21:54:46 -08:00
Zimeng Xiong 448c678ecc Merge pull request #4 from ZimengXiong/ZimengXiong-readme-license
Update license badge in README.md
2025-11-22 21:53:55 -08:00
Zimeng Xiong e980b96091 Merge pull request #4 from ZimengXiong/ZimengXiong-readme-license
Update license badge in README.md
2025-11-22 21:53:55 -08:00
Zimeng Xiong fabe0fcd54 Update license badge in README.md 2025-11-22 21:53:38 -08:00
Zimeng Xiong ef27256879 Update license badge in README.md 2025-11-22 21:53:38 -08:00
Zimeng Xiong c1da41474f Create LICENSE 2025-11-22 21:51:20 -08:00
Zimeng Xiong 815dcd5c80 Create LICENSE 2025-11-22 21:51:20 -08:00
Zimeng Xiong 29936417fc convert all sync op to async, implemented streaming 2025-11-22 21:36:02 -08:00
Zimeng Xiong 49e32f7d96 validate SQlite magic header 2025-11-22 21:27:34 -08:00
Zimeng Xiong cd9c242983 filter with dompurify 2025-11-22 21:21:28 -08:00
Zimeng Xiong 3835557e67 update nginx config 2025-11-22 21:06:01 -08:00
Zimeng Xiong 69bffab745 fix XSS and Root execution of NPM in docker 2025-11-22 20:38:40 -08:00
Zimeng Xiong ef412a3887 Merge pull request #2 from ZimengXiong/fix-bind-mount-prisma
fix bind mount prisma, auto hydrate empty folder
2025-11-22 20:25:44 -08:00
Zimeng Xiong 2e2b4ca455 fix bind mount prisma, auto hydrate empty folder 2025-11-22 20:25:07 -08:00
Zimeng Xiong fb5fe1235c add fallback for browsers that do not have crypto.randomUUID 2025-11-22 19:18:05 -08:00
Zimeng Xiong e21cdbe6a8 add CORS fallback 2025-11-22 19:14:55 -08:00
Zimeng Xiong 94f33f0a56 fix: add linux-musl-openssl-3.0.x 2025-11-22 19:07:28 -08:00
Zimeng Xiong 5d5e22c8a1 fix: pinning CORS to FRONTEND_URL, validate drawing payloads with Zod, staging SQLite imports with integrity checks and atomic swaps in index.ts 2025-11-22 17:17:50 -08:00
Zimeng Xiong b3dbcc2376 Update caution note in README
Added cautionary note about security and production use.
2025-11-22 16:33:23 -08:00
53 changed files with 6988 additions and 484 deletions
+661
View File
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -1,8 +1,9 @@
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
# ExcaliDash v0.1.0
# ExcaliDash v0.1.6
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
![License](https://img.shields.io/github/license/zimengxiong/ExcaliDash)
![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)
[![Docker](https://img.shields.io/badge/docker-ready-blue.svg)](https://hub.docker.com)
A self-hosted dashboard and organizer for [Excalidraw](https://github.com/excalidraw/excalidraw) with live collaboration features.
@@ -74,7 +75,10 @@ See [release notes](https://github.com/ZimengXiong/ExcaliDash/releases) for a sp
# Installation
> [!CAUTION]
> NOT for production use. This is just a side project (and also the first release), and it likely contains some bugs. DO NOT open ports to the internet (e.g. CORS is set to allow all)
> NOT for production use. While attempts have been made at hardening (XSS/dompurify, CORS, rate-limiting, sanitization), they are inadequate for public deployment. Do not expose any ports. Currently lacking CSRF.
> [!CAUTION]
> ExcaliDash is in BETA. Please backup your data regularly (e.g. with cron).
## Docker Hub (Recommended)
+30
View File
@@ -0,0 +1,30 @@
# ExcaliDash v0.1.5
Date: 2025-11-23
Compatibility: v0.1.x (Backward Compatible)
# Security
- RCE: implemented strict Zod schema validation and input sanitization on file uploads; added path traversal guards to file handling logic
- XSS: used DOMPurify for HTML sanitization; blocked execution-capable SVG attributes and enforces CSP headers.
- DoS: moved CPU-intensive operations to worker threads to prevent event loop blocking; request rate limiting (1,000 req/15 min per IP) and streaming for large files
# Infras & Deployment
- non-root execution (uid 1001) in containers
- migrated to multi-stage Docker builds
# Database
- migrated to better-sqlite3, converted all DB interactions to non-blocking async operations and offloaded integrity checks to worker threads.
- implemented SQLite magic header validation; added automatic backup triggers preceding data import
- input validation logic
# Frontend
- updated Settings UI to show version
-202
View File
@@ -1,202 +0,0 @@
# Security Fixes Implementation Summary
## Overview
This document summarizes the comprehensive security fixes implemented to address two critical security vulnerabilities identified in ExcaliDash:
1. **Stored XSS Vector (High Severity)** - Data sanitization negligence
2. **Root Execution Privilege (Critical Severity)** - Container escape risk
## Security Issues Fixed
### Issue 1: Stored XSS Vector (High Severity) ✅ FIXED
**Problem**: Backend used lazy `z.object({}).passthrough()` validation for elements and appState, allowing arbitrary JSON storage without sanitization.
**Attack Vectors**:
- Malicious `.excalidraw` files containing `<script>` tags in element properties
- `javascript:` URIs in link attributes
- SVG previews with embedded malicious code
- Compromised clients sending XSS payloads
**Solution Implemented**:
- **Strict Zod Schemas**: Replaced `.passthrough()` with detailed validation schemas for elements and appState
- **HTML/JS Sanitization**: Implemented comprehensive sanitization layer removing script tags, event handlers, and malicious URLs
- **SVG Sanitization**: Special handling for SVG content to prevent script execution
- **URL Validation**: Whitelist-only approach for URL schemes (http, https, mailto, relative paths only)
- **Input Sanitization**: All string inputs are sanitized before database persistence
- **Import Validation**: Additional security checks for imported .excalidraw files with `X-Imported-File` header
### Issue 2: Root Execution Privilege (Critical Severity) ✅ FIXED
**Problem**: Container ran Node.js process as root without USER directive, providing immediate root access in case of RCE.
**Attack Vectors**:
- RCE vulnerabilities in `better-sqlite3` native bindings
- File upload processing vulnerabilities
- Import functionality exploits
**Solution Implemented**:
- **Non-Root User**: Created dedicated `nodejs` user with UID 1001
- **Permission Management**: Proper ownership and permissions for data directories
- **Dockerfile Security**: Added USER directive to switch to non-root execution
- **Entry Point Security**: Updated docker-entrypoint.sh to handle permissions correctly
### Additional Security Hardening ✅ IMPLEMENTED
**Security Headers**:
- Content Security Policy (CSP) with strict source restrictions
- X-Frame-Options: DENY (prevents clickjacking)
- X-Content-Type-Options: nosniff
- X-XSS-Protection: 1; mode=block
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: geolocation=(), microphone=(), camera=()
**Rate Limiting**:
- Implemented basic rate limiting (1000 requests per 15-minute window)
- Per-IP tracking to prevent DoS attacks
**Request Validation**:
- Maintained existing 50MB request size limits
- Enhanced validation for file imports
## Files Modified
### Backend Changes
1. **`backend/src/security.ts`** - New security utilities module
- HTML/JS sanitization functions
- SVG sanitization functions
- Strict Zod schemas for elements and appState
- Drawing data validation and sanitization
- URL sanitization with whitelist validation
2. **`backend/src/index.ts`** - Updated backend security
- Replaced lazy `.passthrough()` schemas with strict validation
- Added security middleware with headers and rate limiting
- Enhanced POST /drawings endpoint with import validation
- Added malicious content detection and rejection
3. **`backend/Dockerfile`** - Container security hardening
- Created non-root `nodejs` user (UID 1001)
- Added USER directive for non-root execution
- Set proper file ownership and permissions
4. **`backend/docker-entrypoint.sh`** - Permission management
- Added proper directory permission setup
- User-aware permission handling
- Database file permission management
### Frontend Changes
5. **`frontend/src/utils/importUtils.ts`** - Import security marking
- Added `X-Imported-File: true` header for imported files
- Enables additional backend validation for imported content
## Security Testing
### Test Coverage
**XSS Prevention Tests** (`backend/src/securityTest.ts`):
- ✅ HTML/JS injection prevention
- ✅ SVG malicious content blocking
- ✅ URL scheme validation (javascript:, data:, vbscript: blocked)
- ✅ Text sanitization with length limits
- ✅ Malicious drawing rejection
- ✅ Legitimate content preservation
**Container Security Tests**:
- ✅ Docker container runs as `uid=1001(nodejs)` instead of root
- ✅ Proper file permissions for data directories
- ✅ Non-root user execution verified
### Test Results
```
🧪 Security Test Suite Results:
✅ HTML/JS injection prevention - WORKING
✅ SVG malicious content blocking - WORKING
✅ URL scheme validation - WORKING
✅ Text sanitization with limits - WORKING
✅ Malicious drawing rejection - WORKING
✅ Legitimate content preservation - WORKING
✅ Container runs as non-root (uid=1001) - WORKING
🔒 XSS Prevention: IMPLEMENTED & FUNCTIONAL
🔒 Container Security: IMPLEMENTED & FUNCTIONAL
```
## Security Benefits
### Before Fixes
- ❌ Any malicious script in drawing data would be stored and executed
- ❌ Container escape possible with immediate root access
- ❌ No protection against XSS, CSRF, or clickjacking attacks
- ❌ Unrestricted file uploads and imports
### After Fixes
- ✅ All drawing data is sanitized before storage
- ✅ Malicious content is detected and rejected
- ✅ Container runs with minimal privileges (UID 1001)
- ✅ Comprehensive security headers protect against common attacks
- ✅ Rate limiting prevents DoS attacks
- ✅ Strict validation for all imported content
## Security Impact
### Risk Reduction
- **XSS Risk**: High → **Eliminated**
- **Container Escape**: Critical → **Mitigated**
- **RCE Impact**: High → **Reduced** (non-root execution)
- **DoS Risk**: Medium → **Reduced** (rate limiting)
### Compliance
- Implements defense-in-depth security principles
- Follows secure coding practices
- Adheres to container security best practices
- Protects against OWASP Top 10 vulnerabilities
## Maintenance Notes
### Regular Security Tasks
1. **Security Test Suite**: Run `npm run security-test` to verify XSS prevention
2. **Container Security**: Verify non-root execution in production
3. **Dependency Updates**: Keep dependencies updated for security patches
4. **Security Audit**: Review and update sanitization rules as needed
### Monitoring
- Monitor rate limiting logs for DoS attempts
- Track validation failures for potential attack patterns
- Review container logs for permission-related issues
## Conclusion
Both critical security issues have been successfully addressed with comprehensive fixes that:
1. **Eliminate XSS vulnerabilities** through strict validation and sanitization
2. **Reduce container escape risk** through non-root execution
3. **Add defense-in-depth** security measures
4. **Maintain full functionality** while improving security posture
The implementation includes thorough testing to ensure security measures work correctly while preserving legitimate functionality.
**Security Status**: ✅ **RESOLVED**
+1
View File
@@ -0,0 +1 @@
0.1.6
+1
View File
@@ -2,3 +2,4 @@
PORT=8000
NODE_ENV=production
DATABASE_URL=file:/app/prisma/dev.db
FRONTEND_URL=http://localhost:6767
+10 -10
View File
@@ -8,7 +8,10 @@ COPY package*.json ./
COPY tsconfig.json ./
# Install dependencies
RUN npm ci
# Install build deps required for compiling native modules like better-sqlite3
RUN apk add --no-cache python3 make g++ build-base sqlite-dev && \
npm ci
ENV PYTHON=/usr/bin/python3
# Copy prisma schema
COPY prisma ./prisma/
@@ -25,8 +28,8 @@ RUN npx tsc
# Production stage
FROM node:20-alpine
# Install OpenSSL for Prisma and create non-root user
RUN apk add --no-cache openssl && \
# Install OpenSSL for Prisma and su-exec, create non-root user
RUN apk add --no-cache openssl su-exec sqlite-libs && \
addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
@@ -51,17 +54,14 @@ COPY --from=builder /app/src/generated ./dist/generated
# Generate Prisma Client in production (updates node_modules)
RUN npx prisma generate
# Create necessary directories and set proper ownership
RUN mkdir -p /app/uploads /app/prisma && \
chown -R nodejs:nodejs /app
# Create necessary directories (ownership will be set in entrypoint)
RUN mkdir -p /app/uploads /app/prisma
# Copy and set permissions for entrypoint script
COPY docker-entrypoint.sh ./
RUN chmod +x docker-entrypoint.sh && \
chown nodejs:nodejs docker-entrypoint.sh
RUN chmod +x docker-entrypoint.sh
# Switch to non-root user
USER nodejs
# REMOVED: USER nodejs (We must stay root to fix permissions in entrypoint)
EXPOSE 8000
+21 -23
View File
@@ -1,36 +1,34 @@
#!/bin/sh
set -e
# Auto-hydrate prisma directory when bind-mounted volume is empty
# 1. Hydrate volume if empty (Running as root)
if [ ! -f "/app/prisma/schema.prisma" ]; then
echo "Mount is empty. Hydrating /app/prisma from /app/prisma_template..."
cp -R /app/prisma_template/. /app/prisma/
echo "Mount is empty. Hydrating /app/prisma..."
cp -R /app/prisma_template/. /app/prisma/
else
# Volume exists but may be missing new migrations from an upgrade
# Always sync schema and migrations from template to ensure upgrades work
echo "Syncing schema and migrations from template..."
cp /app/prisma_template/schema.prisma /app/prisma/schema.prisma
cp -R /app/prisma_template/migrations/. /app/prisma/migrations/
fi
# Ensure proper ownership and permissions for data directories
echo "Setting up data directory permissions..."
mkdir -p /app/uploads
mkdir -p /app/prisma
# Set ownership to the node user (UID 1000)
if [ "$(id -u)" = "0" ]; then
# If running as root (for some reason), fix ownership
chown -R nodejs:nodejs /app/uploads
chown -R nodejs:nodejs /app/prisma
fi
# 2. Fix permissions unconditionally (Running as root)
echo "Fixing filesystem permissions..."
chown -R nodejs:nodejs /app/uploads
chown -R nodejs:nodejs /app/prisma
chmod 755 /app/uploads
# Ensure database file has proper permissions
if [ -f "/app/prisma/dev.db" ]; then
chmod 664 /app/prisma/dev.db 2>/dev/null || true
echo "Database file found, ensuring write permissions..."
chmod 666 /app/prisma/dev.db
fi
# Set appropriate permissions for uploads directory
chmod 755 /app/uploads
# Run migrations as the current user
# 3. Run Migrations (Drop privileges to nodejs)
echo "Running database migrations..."
npx prisma migrate deploy
su-exec nodejs npx prisma migrate deploy
# Start the application
echo "Starting application as user $(whoami) (UID: $(id -u))"
node dist/index.js
# 4. Start Application (Drop privileges to nodejs)
echo "Starting application as nodejs..."
exec su-exec nodejs node dist/index.js
+1 -8
View File
@@ -22,6 +22,7 @@
"express": "^5.1.0",
"jsdom": "^27.2.0",
"multer": "^2.0.2",
"prisma": "^5.22.0",
"socket.io": "^4.8.1",
"zod": "^4.1.12"
},
@@ -30,7 +31,6 @@
"@types/express": "^5.0.5",
"@types/node": "^24.10.1",
"nodemon": "^3.1.11",
"prisma": "^5.22.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
}
@@ -312,14 +312,12 @@
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -333,14 +331,12 @@
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "5.22.0",
@@ -352,7 +348,6 @@
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "5.22.0"
@@ -1747,7 +1742,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -2677,7 +2671,6 @@
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "backend",
"version": "1.0.0",
"version": "0.1.6",
"description": "",
"main": "index.js",
"scripts": {
@@ -25,6 +25,7 @@
"express": "^5.1.0",
"jsdom": "^27.2.0",
"multer": "^2.0.2",
"prisma": "^5.22.0",
"socket.io": "^4.8.1",
"zod": "^4.1.12"
},
@@ -33,7 +34,6 @@
"@types/express": "^5.0.5",
"@types/node": "^24.10.1",
"nodemon": "^3.1.11",
"prisma": "^5.22.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
}
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
-- CreateTable
CREATE TABLE "Library" (
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'default',
"items" TEXT NOT NULL DEFAULT '[]',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
@@ -0,0 +1,34 @@
-- CreateTable
CREATE TABLE "PrivateVault" (
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'vault',
"passwordHash" TEXT NOT NULL,
"salt" TEXT NOT NULL,
"hint" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Drawing" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"elements" TEXT NOT NULL,
"appState" TEXT NOT NULL,
"files" TEXT NOT NULL DEFAULT '{}',
"preview" TEXT,
"version" INTEGER NOT NULL DEFAULT 1,
"collectionId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"isPrivate" BOOLEAN NOT NULL DEFAULT false,
"encryptedData" TEXT,
"iv" TEXT,
CONSTRAINT "Drawing_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Drawing" ("appState", "collectionId", "createdAt", "elements", "files", "id", "name", "preview", "updatedAt", "version") SELECT "appState", "collectionId", "createdAt", "elements", "files", "id", "name", "preview", "updatedAt", "version" FROM "Drawing";
DROP TABLE "Drawing";
ALTER TABLE "new_Drawing" RENAME TO "Drawing";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
Binary file not shown.
+22
View File
@@ -32,4 +32,26 @@ model Drawing {
collection Collection? @relation(fields: [collectionId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Privacy/Encryption fields
isPrivate Boolean @default(false)
encryptedData String? // Encrypted blob containing elements, appState, files when isPrivate=true
iv String? // Initialization vector for AES-GCM decryption
}
// Singleton model for storing vault password hash and settings
model PrivateVault {
id String @id @default("vault") // Singleton pattern
passwordHash String // bcrypt hash for password verification
salt String // Salt for client-side key derivation (hex encoded)
hint String? // Optional password hint
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Library {
id String @id @default("default") // Singleton pattern - use "default" ID
items String @default("[]") // Stored as JSON string array of library items
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
File diff suppressed because one or more lines are too long
+22 -1
View File
@@ -136,6 +136,25 @@ exports.Prisma.DrawingScalarFieldEnum = {
version: 'version',
collectionId: 'collectionId',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
isPrivate: 'isPrivate',
encryptedData: 'encryptedData',
iv: 'iv'
};
exports.Prisma.PrivateVaultScalarFieldEnum = {
id: 'id',
passwordHash: 'passwordHash',
salt: 'salt',
hint: 'hint',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.LibraryScalarFieldEnum = {
id: 'id',
items: 'items',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
@@ -152,7 +171,9 @@ exports.Prisma.NullsOrder = {
exports.Prisma.ModelName = {
Collection: 'Collection',
Drawing: 'Drawing'
Drawing: 'Drawing',
PrivateVault: 'PrivateVault',
Library: 'Library'
};
/**
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "prisma-client-6afe3d9baa793154c8d01c79f8418d91423e5ccaec794547bf848a451459cf53",
"name": "prisma-client-2894d2d911743eb6e6a7a673efae7513c10b6ce609edeb9caf76ed42f4728682",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",
@@ -32,4 +32,26 @@ model Drawing {
collection Collection? @relation(fields: [collectionId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Privacy/Encryption fields
isPrivate Boolean @default(false)
encryptedData String? // Encrypted blob containing elements, appState, files when isPrivate=true
iv String? // Initialization vector for AES-GCM decryption
}
// Singleton model for storing vault password hash and settings
model PrivateVault {
id String @id @default("vault") // Singleton pattern
passwordHash String // bcrypt hash for password verification
salt String // Salt for client-side key derivation (hex encoded)
hint String? // Optional password hint
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Library {
id String @id @default("default") // Singleton pattern - use "default" ID
items String @default("[]") // Stored as JSON string array of library items
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+22 -1
View File
@@ -136,6 +136,25 @@ exports.Prisma.DrawingScalarFieldEnum = {
version: 'version',
collectionId: 'collectionId',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
isPrivate: 'isPrivate',
encryptedData: 'encryptedData',
iv: 'iv'
};
exports.Prisma.PrivateVaultScalarFieldEnum = {
id: 'id',
passwordHash: 'passwordHash',
salt: 'salt',
hint: 'hint',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.LibraryScalarFieldEnum = {
id: 'id',
items: 'items',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
@@ -152,7 +171,9 @@ exports.Prisma.NullsOrder = {
exports.Prisma.ModelName = {
Collection: 'Collection',
Drawing: 'Drawing'
Drawing: 'Drawing',
PrivateVault: 'PrivateVault',
Library: 'Library'
};
/**
+624 -42
View File
File diff suppressed because it is too large Load Diff
+120 -93
View File
@@ -257,133 +257,160 @@ export const sanitizeUrl = (url: unknown): string => {
};
/**
* Strict Zod schema for Excalidraw elements with validation
* Very flexible Zod schema for Excalidraw elements
*/
export const elementSchema = z
.object({
id: z.string().min(1).max(100),
type: z.enum([
"rectangle",
"ellipse",
"diamond",
"arrow",
"line",
"text",
"image",
"frame",
"embed",
"selection",
"text-container",
]),
x: z.number().finite().min(-100000).max(100000),
y: z.number().finite().min(-100000).max(100000),
width: z.number().finite().min(0).max(100000),
height: z.number().finite().min(0).max(100000),
angle: z
.number()
.finite()
.min(-2 * Math.PI)
.max(2 * Math.PI),
strokeColor: z.string().optional(),
backgroundColor: z.string().optional(),
fillStyle: z.enum(["solid", "hachure", "cross-hatch", "dots"]).optional(),
strokeWidth: z.number().finite().min(0).max(10).optional(),
strokeStyle: z.enum(["solid", "dashed", "dotted"]).optional(),
roundness: z
.object({
type: z.enum(["round", "sharp"]),
value: z.number().finite().min(0).max(1),
})
.optional(),
boundElements: z
.array(
z.object({
id: z.string(),
type: z.string(),
})
)
.optional(),
groupIds: z.array(z.string()).optional(),
frameId: z.string().optional(),
seed: z.number().finite().optional(),
version: z.number().finite().min(0).max(100000),
versionNonce: z.number().finite().min(0).max(100000),
isDeleted: z.boolean().optional(),
opacity: z.number().finite().min(0).max(1).optional(),
link: z.string().optional().transform(sanitizeUrl),
locked: z.boolean().optional(),
// Text-specific properties
text: z
.string()
.optional()
.transform((val) => sanitizeText(val, 5000)),
fontSize: z.number().finite().min(1).max(200).optional(),
fontFamily: z.number().finite().min(1).max(5).optional(),
textAlign: z.enum(["left", "center", "right"]).optional(),
verticalAlign: z.enum(["top", "middle", "bottom"]).optional(),
// Custom properties - whitelist only known safe properties
customData: z.record(z.string(), z.any()).optional(),
id: z.string().min(1).max(200).optional().nullable(),
type: z.string().optional().nullable(),
x: z.number().optional().nullable(),
y: z.number().optional().nullable(),
width: z.number().optional().nullable(),
height: z.number().optional().nullable(),
angle: z.number().optional().nullable(),
strokeColor: z.string().optional().nullable(),
backgroundColor: z.string().optional().nullable(),
fillStyle: z.string().optional().nullable(),
strokeWidth: z.number().optional().nullable(),
strokeStyle: z.string().optional().nullable(),
roundness: z.any().optional().nullable(),
boundElements: z.array(z.any()).optional().nullable(),
groupIds: z.array(z.string()).optional().nullable(),
frameId: z.string().optional().nullable(),
seed: z.number().optional().nullable(),
version: z.number().optional().nullable(),
versionNonce: z.number().optional().nullable(),
isDeleted: z.boolean().optional().nullable(),
opacity: z.number().optional().nullable(),
link: z.string().optional().nullable(),
locked: z.boolean().optional().nullable(),
text: z.string().optional().nullable(),
fontSize: z.number().optional().nullable(),
fontFamily: z.number().optional().nullable(),
textAlign: z.string().optional().nullable(),
verticalAlign: z.string().optional().nullable(),
customData: z.record(z.string(), z.any()).optional().nullable(),
})
.strict();
.passthrough()
.transform((element) => {
// Apply basic sanitization to string values only
const sanitized = { ...element };
if (typeof sanitized.text === "string") {
sanitized.text = sanitizeText(sanitized.text, 5000);
}
if (typeof sanitized.link === "string") {
sanitized.link = sanitizeUrl(sanitized.link);
}
return sanitized;
});
/**
* Strict Zod schema for Excalidraw app state with validation
* Flexible Zod schema for Excalidraw app state with validation
*/
export const appStateSchema = z
.object({
gridSize: z.number().finite().min(0).max(100).optional(),
gridStep: z.number().finite().min(1).max(100).optional(),
viewBackgroundColor: z.string().optional(),
currentItemStrokeColor: z.string().optional(),
currentItemBackgroundColor: z.string().optional(),
gridSize: z.number().finite().min(0).max(1000).optional().nullable(),
gridStep: z.number().finite().min(1).max(1000).optional().nullable(),
viewBackgroundColor: z.string().optional().nullable(),
currentItemStrokeColor: z.string().optional().nullable(),
currentItemBackgroundColor: z.string().optional().nullable(),
currentItemFillStyle: z
.enum(["solid", "hachure", "cross-hatch", "dots"])
.optional(),
currentItemStrokeWidth: z.number().finite().min(0).max(10).optional(),
currentItemStrokeStyle: z.enum(["solid", "dashed", "dotted"]).optional(),
.optional()
.nullable(),
currentItemStrokeWidth: z
.number()
.finite()
.min(0)
.max(50)
.optional()
.nullable(),
currentItemStrokeStyle: z
.enum(["solid", "dashed", "dotted"])
.optional()
.nullable(),
currentItemRoundness: z
.object({
type: z.enum(["round", "sharp"]),
value: z.number().finite().min(0).max(1),
})
.optional(),
currentItemFontSize: z.number().finite().min(1).max(200).optional(),
currentItemFontFamily: z.number().finite().min(1).max(5).optional(),
currentItemTextAlign: z.enum(["left", "center", "right"]).optional(),
currentItemVerticalAlign: z.enum(["top", "middle", "bottom"]).optional(),
scrollX: z.number().finite().min(-1000000).max(1000000).optional(),
scrollY: z.number().finite().min(-1000000).max(1000000).optional(),
.optional()
.nullable(),
currentItemFontSize: z
.number()
.finite()
.min(1)
.max(500)
.optional()
.nullable(),
currentItemFontFamily: z
.number()
.finite()
.min(1)
.max(10)
.optional()
.nullable(),
currentItemTextAlign: z
.enum(["left", "center", "right"])
.optional()
.nullable(),
currentItemVerticalAlign: z
.enum(["top", "middle", "bottom"])
.optional()
.nullable(),
scrollX: z
.number()
.finite()
.min(-10000000)
.max(10000000)
.optional()
.nullable(),
scrollY: z
.number()
.finite()
.min(-10000000)
.max(10000000)
.optional()
.nullable(),
zoom: z
.object({
value: z.number().finite().min(0.1).max(10),
value: z.number().finite().min(0.01).max(100),
})
.optional(),
selection: z.array(z.string()).optional(),
selectedElementIds: z.record(z.string(), z.boolean()).optional(),
selectedGroupIds: z.record(z.string(), z.boolean()).optional(),
.optional()
.nullable(),
selection: z.array(z.string()).optional().nullable(),
selectedElementIds: z.record(z.string(), z.boolean()).optional().nullable(),
selectedGroupIds: z.record(z.string(), z.boolean()).optional().nullable(),
activeEmbeddable: z
.object({
elementId: z.string(),
state: z.string(),
})
.optional(),
.optional()
.nullable(),
activeTool: z
.object({
type: z.string(),
customType: z.string().optional(),
customType: z.string().optional().nullable(),
})
.optional(),
cursorX: z.number().finite().optional(),
cursorY: z.number().finite().optional(),
// Sanitize any string values in appState
.optional()
.nullable(),
cursorX: z.number().finite().optional().nullable(),
cursorY: z.number().finite().optional().nullable(),
// Add common Excalidraw app state properties
collaborators: z.record(z.string(), z.any()).optional().nullable(),
})
.strict()
// Allow any additional properties
.catchall(
z.any().refine((val) => {
// Recursively sanitize any string values found in the object
// Sanitize string values, but be more permissive for other types
if (typeof val === "string") {
return sanitizeText(val, 1000);
}
// Allow numbers, booleans, objects, arrays, null, undefined
return true;
})
);

Some files were not shown because too many files have changed in this diff Show More