mirror of
https://github.com/zadam/trilium.git
synced 2025-11-07 05:46:10 +01:00
464 lines
9.5 KiB
Markdown
Vendored
464 lines
9.5 KiB
Markdown
Vendored
# Security
|
|
Trilium implements a **defense-in-depth security model** with multiple layers of protection for user data. The security architecture covers authentication, authorization, encryption, input sanitization, and secure communication.
|
|
|
|
## Security Principles
|
|
|
|
1. **Data Privacy**: User data is protected at rest and in transit
|
|
2. **Encryption**: Per-note encryption for sensitive content
|
|
3. **Authentication**: Multiple authentication methods supported
|
|
4. **Authorization**: Single-user model with granular protected sessions
|
|
5. **Input Validation**: All user input sanitized
|
|
6. **Secure Defaults**: Security features enabled by default
|
|
7. **Transparency**: Open source allows security audits
|
|
|
|
## Threat Model
|
|
|
|
### Threats Considered
|
|
|
|
1. **Unauthorized Access**
|
|
* Physical access to device
|
|
* Network eavesdropping
|
|
* Stolen credentials
|
|
* Session hijacking
|
|
2. **Data Exfiltration**
|
|
* Malicious scripts
|
|
* XSS attacks
|
|
* SQL injection
|
|
* CSRF attacks
|
|
3. **Data Corruption**
|
|
* Malicious modifications
|
|
* Database tampering
|
|
* Sync conflicts
|
|
4. **Privacy Leaks**
|
|
* Unencrypted backups
|
|
* Search indexing
|
|
* Temporary files
|
|
* Memory dumps
|
|
|
|
### Out of Scope
|
|
|
|
* Nation-state attackers
|
|
* Zero-day vulnerabilities in dependencies
|
|
* Hardware vulnerabilities (Spectre, Meltdown)
|
|
* Physical access with unlimited time
|
|
* Quantum computing attacks
|
|
|
|
## Authentication
|
|
|
|
### Password Authentication
|
|
|
|
**Implementation:** `apps/server/src/services/password.ts`
|
|
|
|
### TOTP (Two-Factor Authentication)
|
|
|
|
**Implementation:** `apps/server/src/routes/api/login.ts`
|
|
|
|
### OpenID Connect
|
|
|
|
**Implementation:** `apps/server/src/routes/api/login.ts`
|
|
|
|
**Supported Providers:**
|
|
|
|
* Any OpenID Connect compatible provider
|
|
* Google, GitHub, Auth0, etc.
|
|
|
|
**Flow:**
|
|
|
|
```typescript
|
|
// 1. Redirect to provider
|
|
GET /api/login/openid
|
|
|
|
// 2. Provider redirects back with code
|
|
GET /api/login/openid/callback?code=...
|
|
|
|
// 3. Exchange code for tokens
|
|
const tokens = await openidClient.callback(redirectUri, req.query)
|
|
|
|
// 4. Verify ID token
|
|
const claims = tokens.claims()
|
|
|
|
// 5. Create session
|
|
req.session.loggedIn = true
|
|
```
|
|
|
|
### Session Management
|
|
|
|
**Session Storage:** SQLite database (sessions table)
|
|
|
|
**Session Configuration:**
|
|
|
|
```typescript
|
|
app.use(session({
|
|
secret: sessionSecret,
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
rolling: true,
|
|
cookie: {
|
|
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
httpOnly: true,
|
|
secure: isHttps,
|
|
sameSite: 'lax'
|
|
},
|
|
store: new SqliteStore({
|
|
db: db,
|
|
table: 'sessions'
|
|
})
|
|
}))
|
|
```
|
|
|
|
**Session Invalidation:**
|
|
|
|
* Automatic timeout after inactivity
|
|
* Manual logout clears session
|
|
* Server restart invalidates all sessions (optional)
|
|
|
|
## Authorization
|
|
|
|
### Single-User Model
|
|
|
|
**Desktop:**
|
|
|
|
* Single user (owner of device)
|
|
* No multi-user support
|
|
* Full access to all notes
|
|
|
|
**Server:**
|
|
|
|
* Single user per installation
|
|
* Authentication required for all operations
|
|
* No user roles or permissions
|
|
|
|
### Protected Sessions
|
|
|
|
**Purpose:** Temporary access to encrypted (protected) notes
|
|
|
|
**Implementation:** `apps/server/src/services/protected_session.ts`
|
|
|
|
**Workflow:**
|
|
|
|
```typescript
|
|
// 1. User enters password for protected notes
|
|
POST /api/protected-session/enter
|
|
Body: { password: "protected-password" }
|
|
|
|
// 2. Derive encryption key
|
|
const protectedDataKey = deriveKey(password)
|
|
|
|
// 3. Verify password (decrypt known encrypted value)
|
|
const decrypted = decrypt(testValue, protectedDataKey)
|
|
if (decrypted === expectedValue) {
|
|
// 4. Store in memory (not in session)
|
|
protectedSessionHolder.setProtectedDataKey(protectedDataKey)
|
|
|
|
// 5. Set timeout
|
|
setTimeout(() => {
|
|
protectedSessionHolder.clearProtectedDataKey()
|
|
}, timeout)
|
|
}
|
|
```
|
|
|
|
**Protected Session Timeout:**
|
|
|
|
* Default: 10 minutes (configurable)
|
|
* Extends on activity
|
|
* Cleared on browser close
|
|
* Separate from main session
|
|
|
|
### API Authorization
|
|
|
|
**Internal API:**
|
|
|
|
* Requires authenticated session
|
|
* CSRF token validation
|
|
* Same-origin policy
|
|
|
|
**ETAPI (External API):**
|
|
|
|
* Token-based authentication
|
|
* No session required
|
|
* Rate limiting
|
|
|
|
## Encryption
|
|
|
|
### Note Encryption
|
|
|
|
**Encryption Algorithm:** AES-256-CBC
|
|
|
|
**Key Hierarchy:**
|
|
|
|
```
|
|
User Password
|
|
↓ (scrypt)
|
|
Data Key (for protected notes)
|
|
↓ (AES-128)
|
|
Protected Note Content
|
|
```
|
|
|
|
**Protected Note Metadata:**
|
|
|
|
* Content IS encrypted
|
|
* Type and MIME are NOT encrypted
|
|
* Attributes are NOT encrypted
|
|
|
|
### Data Key Management
|
|
|
|
**Key Rotation:**
|
|
|
|
* Not currently supported
|
|
* Requires re-encrypting all protected notes
|
|
|
|
### Transport Encryption
|
|
|
|
**HTTPS:**
|
|
|
|
* Recommended for server installations
|
|
* TLS 1.2+ only
|
|
* Strong cipher suites preferred
|
|
* Certificate validation enabled
|
|
|
|
**Desktop:**
|
|
|
|
* Local communication (no network)
|
|
* No HTTPS required
|
|
|
|
### Backup Encryption
|
|
|
|
**Database Backups:**
|
|
|
|
* Protected notes remain encrypted in backup
|
|
* Backup file should be protected separately
|
|
* Consider encrypting backup storage location
|
|
|
|
## Input Sanitization
|
|
|
|
### XSS Prevention
|
|
|
|
* **HTML Sanitization**
|
|
* **CKEditor Configuration:**
|
|
|
|
```
|
|
// apps/client/src/widgets/type_widgets/text_type_widget.ts
|
|
ClassicEditor.create(element, {
|
|
// Restrict allowed content
|
|
htmlSupport: {
|
|
allow: [
|
|
{ name: /./, attributes: true, classes: true, styles: true }
|
|
],
|
|
disallow: [
|
|
{ name: 'script' },
|
|
{ name: 'iframe', attributes: /^(?!src$).*/ }
|
|
]
|
|
}
|
|
})
|
|
```
|
|
* Content Security Policy
|
|
|
|
### SQL Injection Prevention
|
|
|
|
**Parameterized Queries:**
|
|
|
|
```typescript
|
|
const notes = sql.getRows(
|
|
'SELECT * FROM notes WHERE title = ?',
|
|
[userInput]
|
|
)
|
|
```
|
|
|
|
**ORM Usage:**
|
|
|
|
```typescript
|
|
// Entity-based access prevents SQL injection
|
|
const note = becca.getNote(noteId)
|
|
note.title = userInput // Sanitized by entity
|
|
note.save() // Parameterized query
|
|
```
|
|
|
|
### CSRF Prevention
|
|
|
|
**CSRF Token Validation:**
|
|
|
|
Location: `apps/server/src/routes/csrf_protection.ts`
|
|
|
|
Stateless CSRF using [Double Submit Cookie Pattern](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie) via [`csrf-csrf`](https://github.com/Psifi-Solutions/csrf-csrf).
|
|
|
|
### File Upload Validation
|
|
|
|
**Validation:**
|
|
|
|
```typescript
|
|
// Validate file size
|
|
const maxSize = 100 * 1024 * 1024 // 100 MB
|
|
if (file.size > maxSize) {
|
|
throw new Error('File too large')
|
|
}
|
|
```
|
|
|
|
## Network Security
|
|
|
|
### HTTPS Configuration
|
|
|
|
**Certificate Validation:**
|
|
|
|
* Require valid certificates in production
|
|
* Self-signed certificates allowed for development
|
|
* Certificate pinning not implemented
|
|
|
|
### Rate Limiting
|
|
|
|
**Login Rate Limiting:**
|
|
|
|
```typescript
|
|
const loginLimiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000,
|
|
max: 10, // 10 failed attempts
|
|
skipSuccessfulRequests: true
|
|
})
|
|
|
|
app.post('/api/login/password', loginLimiter, loginHandler)
|
|
```
|
|
|
|
## Data Security
|
|
|
|
### Secure Data Deletion
|
|
|
|
**Soft Delete:**
|
|
|
|
```typescript
|
|
// Mark as deleted (sync first)
|
|
note.isDeleted = 1
|
|
note.deleteId = generateUUID()
|
|
note.save()
|
|
|
|
// Entity change tracked for sync
|
|
addEntityChange('notes', noteId, note)
|
|
```
|
|
|
|
**Hard Delete (Erase):**
|
|
|
|
```typescript
|
|
// After sync completed
|
|
sql.execute('DELETE FROM notes WHERE noteId = ?', [noteId])
|
|
sql.execute('DELETE FROM branches WHERE noteId = ?', [noteId])
|
|
sql.execute('DELETE FROM attributes WHERE noteId = ?', [noteId])
|
|
|
|
// Mark entity change as erased
|
|
sql.execute('UPDATE entity_changes SET isErased = 1 WHERE entityId = ?', [noteId])
|
|
```
|
|
|
|
**Blob Cleanup:**
|
|
|
|
```typescript
|
|
// Find orphaned blobs (not referenced by any note/revision/attachment)
|
|
const orphanedBlobs = sql.getRows(`
|
|
SELECT blobId FROM blobs
|
|
WHERE blobId NOT IN (SELECT blobId FROM notes WHERE blobId IS NOT NULL)
|
|
AND blobId NOT IN (SELECT blobId FROM revisions WHERE blobId IS NOT NULL)
|
|
AND blobId NOT IN (SELECT blobId FROM attachments WHERE blobId IS NOT NULL)
|
|
`)
|
|
|
|
// Delete orphaned blobs
|
|
for (const blob of orphanedBlobs) {
|
|
sql.execute('DELETE FROM blobs WHERE blobId = ?', [blob.blobId])
|
|
}
|
|
```
|
|
|
|
### Memory Security
|
|
|
|
**Protected Data in Memory:**
|
|
|
|
* Protected data keys stored in memory only
|
|
* Cleared on timeout
|
|
* Not written to disk
|
|
* Not in session storage
|
|
|
|
## Dependency Security
|
|
|
|
### Vulnerability Scanning
|
|
|
|
**Tools:**
|
|
|
|
* Renovate bot - Automatic dependency updates
|
|
* `pnpm audit` - Check for known vulnerabilities
|
|
* GitHub Dependabot alerts
|
|
|
|
**Process:**
|
|
|
|
```
|
|
# Check for vulnerabilities
|
|
npm audit
|
|
|
|
# Fix automatically
|
|
npm audit fix
|
|
|
|
# Manual review for breaking changes
|
|
npm audit fix --force
|
|
```
|
|
|
|
### Dependency Pinning
|
|
|
|
**package.json:**
|
|
|
|
```
|
|
{
|
|
"dependencies": {
|
|
"express": "4.18.2", // Exact version
|
|
"better-sqlite3": "^9.2.2" // Compatible versions
|
|
}
|
|
}
|
|
```
|
|
|
|
**pnpm Overrides:**
|
|
|
|
```
|
|
{
|
|
"pnpm": {
|
|
"overrides": {
|
|
"lodash@<4.17.21": ">=4.17.21", // Force minimum version
|
|
"axios@<0.21.2": ">=0.21.2"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Patch Management
|
|
|
|
**pnpm Patches:**
|
|
|
|
```
|
|
# Create patch
|
|
pnpm patch @ckeditor/ckeditor5
|
|
|
|
# Edit files in temporary directory
|
|
# ...
|
|
|
|
# Generate patch file
|
|
pnpm patch-commit /tmp/ckeditor5-patch
|
|
|
|
# Patch applied automatically on install
|
|
```
|
|
|
|
## Security Auditing
|
|
|
|
### Logs
|
|
|
|
**Security Events Logged:**
|
|
|
|
* Login attempts (success/failure)
|
|
* Protected session access
|
|
* Password changes
|
|
* ETAPI token usage
|
|
* Failed CSRF validations
|
|
|
|
**Log Location:**
|
|
|
|
* Desktop: Console output
|
|
* Server: Log files or stdout
|
|
|
|
### Monitoring
|
|
|
|
**Metrics to Monitor:**
|
|
|
|
* Failed login attempts
|
|
* API error rates
|
|
* Unusual database changes
|
|
* Large exports/imports |