This file defines security rules for the Leaflet project. All contributors and AI agents must follow these.
- NEVER commit
.envfiles to the repository - NEVER hardcode API keys, passwords, secrets, or paths in source code
- All configuration lives in
.env(gitignored) with a.env.exampletracking the required keys - Docker Compose passes env vars from the host - do not bake secrets into Dockerfiles or compose files
- The
data/screenshots/directory is gitignored - screenshots may contain sensitive engagement data
The app serves and writes .md files based on paths provided by the user (via the file tree and API). This is a classic path traversal attack surface.
Every server route in src/routes/api/notes/ must validate file paths before any filesystem operation. The current implementation resolves the path and then checks that it still stays inside the notes root.
userPathmust be validated withsafePath()before anyfs.readFile,fs.writeFile,fs.unlink, orfs.mkdircall- Reject paths containing
.., null bytes (\0), or absolute path segments - Only
.mdfiles are readable/writable via the notes API (enforce.mdextension) data/notes/templates/directory should be readable but not writable via the API
PUT/POSTnote routes accept only Markdown content and resolve the final path relative to the notes directoryGET /api/templatesdoes not touch the filesystem; it validates workspace references and returns database rows onlyGET /api/screenshots?workspaceId=...rejects an empty workspace ID with400PATCH /api/screenshots/[filename]requires a non-emptyworkspaceIdand limits caption and linked note input sizes
- Screenshot files use timestamp-based safe names and are validated before any file access
- The API accepts PNG, JPEG, GIF, and WebP uploads, and the upload size is capped at 10 MB
- Workspace metadata for screenshots is optional on upload, but when supplied it must be a non-empty string
linked_note_pathis stored as data and must not contain..
- The SQLite database file (
data/notes.db) is accessed by server-side code only - No database file path or contents should be exposed via any HTTP response
- Use parameterized queries with
better-sqlite3always - never string-concatenate SQL
// CORRECT
const row = db.prepare("SELECT * FROM workspaces WHERE id = ?").get(id);
// NEVER DO THIS
const row = db.prepare(`SELECT * FROM workspaces WHERE id = ${id}`).get();- All user-provided names (workspace names, hostnames, file names) should be validated:
- Max length (255 chars)
- No null bytes
- No characters that are invalid in file names on Windows and Linux
- Note content is user-controlled markdown - it is NOT sanitized before storage (user owns the data)
- Note content IS sanitized before rendering as HTML to prevent XSS (Milkdown handles this; if rendering via custom HTML, use DOMPurify)
- User template fields must be validated before insert:
titlerequired, max 255 charsdescriptionoptional, max 1000 charscontentrequired, max 500,000 charsworkspaceIdoptional, but if present must resolve to an existing workspace
- Production Dockerfiles use non-root users where possible
docker composebind mounts are limited todata/- not the entire project directory- Production compose file does not expose unnecessary ports
.env
.env.local
.env.*.local
data/notes.db-shm
data/notes.db-wal
data/screenshots/