-
Notifications
You must be signed in to change notification settings - Fork 7
Security
Security features and best practices for Memory Journal MCP Server.
Local-First Design: Your project context stays on your machine. No external API calls, no cloud dependencies - your development history remains private.
Memory Journal is local-first:
- All data stored locally in SQLite
- No external API calls
- No telemetry or analytics
- You own and control your data
const MAX_CONTENT_SIZE = 50 * 1024; // 50KB per entry
const MAX_TAG_LENGTH = 100; // 100 characters per tagWhy limits:
- Prevent resource exhaustion
- Ensure reasonable performance
- Protect database integrity
All queries use parameterized statements:
// Safe - parameterized query
db.run("INSERT INTO memory_journal (content) VALUES (?)", [userContent]);
// NEVER do this (vulnerable to SQL injection)
db.run(`INSERT INTO memory_journal (content) VALUES ('${userContent}')`); // DON'T!Benefits:
- SQL injection impossible
- Safe handling of quotes and special characters
- Database-level validation
Search queries using SQL LIKE clauses have wildcards (%, _, \) escaped to prevent unintended pattern matching:
// User searches for "100%" — safely escaped to "100\%"
sanitizeSearchQuery("100%"); // Returns '100\\%'Backup and restore filenames are validated against path traversal attacks:
// Rejects malicious filenames like "../../etc/passwd"
assertNoPathTraversal(filename); // Throws PathTraversalErrorFilesystem operations via Code Mode (mj_execute_code) and data export tools are hard-blocked by default to prevent unauthorized access.
Administrators must explicitly configure the ALLOWED_IO_ROOTS environment variable with absolute paths to grant access.
# Critical Security Boundary: Comma-separated absolute paths granting filesystem access
export ALLOWED_IO_ROOTS="/path/to/your/git/repo,/path/to/shared/data"Security Warning: Never use generic directories like / or /usr for ALLOWED_IO_ROOTS.
When running in HTTP mode (--transport http), the following security measures apply:
Every HTTP response includes these security headers:
X-Content-Type-Options: nosniffX-Frame-Options: DENYContent-Security-Policy: default-src 'none'; frame-ancestors 'none'Cache-Control: no-store, no-cache, must-revalidateReferrer-Policy: no-referrerPermissions-Policy: camera=(), microphone=(), geolocation=()
HTTP endpoints are rate-limited to 100 requests per minute per IP to prevent abuse:
// Automatic — no configuration needed
// Returns 429 Too Many Requests when exceeded# Restrict CORS to specific origins (recommended)
memory-journal-mcp --transport http --cors-origin "http://localhost:3000"
# Multiple origins (comma-separated)
memory-journal-mcp --transport http --cors-origin "http://localhost:3000,https://app.example.com"Opt-in via --enable-hsts flag. When enabled, sends Strict-Transport-Security: max-age=31536000; includeSubDomains on all responses.
Request bodies are limited to 1MB to prevent memory exhaustion.
- UUID-based session IDs via
crypto.randomUUID() - 30-minute timeout with automatic cleanup
- 5-minute sweep interval for expired sessions
For production HTTP deployments, Memory Journal supports OAuth 2.1 authentication (opt-in):
| Component | Status | Description |
|---|---|---|
| Protected Resource Metadata | ✅ | RFC 9728 /.well-known/oauth-protected-resource
|
| Auth Server Discovery | ✅ | RFC 8414 metadata discovery with caching |
| Token Validation | ✅ | JWT validation with JWKS support (via jose) |
| Scope Enforcement | ✅ | Granular read, write, admin scopes |
| HTTP Middleware | ✅ | Bearer token extraction and validation |
| Scope | Tool Groups | Description |
|---|---|---|
read |
core, search, analytics, relationships, export | Read-only operations |
write |
github, team (+ all read groups) | Read + write operations |
admin |
admin, backup, codemode (+ all write/read groups) | Full administrative access |
# CLI flags
memory-journal-mcp --transport http --port 3000 \
--oauth-enabled \
--oauth-issuer https://auth.example.com/realms/mcp \
--oauth-audience memory-journal-mcp \
--oauth-jwks-uri https://auth.example.com/realms/mcp/protocol/openid-connect/certs
# Or via environment variables
export OAUTH_ENABLED=true
export OAUTH_ISSUER=https://auth.example.com/realms/mcp
export OAUTH_AUDIENCE=memory-journal-mcp
export OAUTH_JWKS_URI=https://auth.example.com/realms/mcp/protocol/openid-connect/certs- Client sends
Authorization: Bearer <token>header - Server validates JWT signature against JWKS endpoint
- Server checks issuer, audience, and expiration claims
- Server extracts scopes from token and enforces tool-level access
- Invalid/missing tokens receive
401withWWW-Authenticateheader
When OAuth is not enabled, authentication falls back to:
-
Simple token auth — if
MCP_AUTH_TOKENenv var is set, requests must includeAuthorization: Bearer <token>matching the configured value - No auth — if no auth mechanism is configured, all requests are accepted (suitable for local/development use)
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
# Switch to non-root user
USER appuserBenefits:
- Container cannot modify host system
- Reduced attack surface
- Follows least privilege principle
FROM node:24-alpineAlpine Linux benefits:
- Small attack surface (~5MB base)
- Security-focused distribution
- Fewer vulnerabilities
- Fast updates
{
"command": "docker",
"args": [
"run",
"--rm",
"-i",
// NO --privileged flag
// NO --cap-add flags
"-v",
"./data:/app/data",
"writenotenow/memory-journal-mcp:latest"
]
}{
"args": [
"-v",
"./data:/app/data:rw" // Only data directory
// NOT: "-v", "/:/host" - would expose entire host
]
}Best practices:
- Mount only necessary directories
- Use read-only mounts where possible
- Avoid mounting sensitive host directories
Recommended (highest security):
# Use SHA-256 digest for immutable reference
docker pull writenotenow/memory-journal-mcp@sha256:abc123...
# Find SHA digests at:
# https://hub.docker.com/r/writenotenow/memory-journal-mcp/tagsBenefits:
- Immutable image reference
- Protection against tag hijacking
- Reproducible deployments
- Supply chain verification
Tag-based (convenience):
docker pull writenotenow/memory-journal-mcp:latestbetter-sqlite3 operates as a native file-based database with WAL mode for concurrent read access.
-- better-sqlite3 uses native file-based database
PRAGMA journal_mode = WAL; -- Write-Ahead Logging for concurrent reads
PRAGMA synchronous = NORMAL; -- Balanced durability/performance
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 30000; -- 30s wait for locksBenefits:
- Data integrity via atomic transactions
- Referential integrity via foreign keys
- WAL mode for concurrent read access
- Native file-based persistence
FOREIGN KEY (entry_id) REFERENCES memory_journal(id) ON DELETE CASCADEBenefits:
- Referential integrity
- Automatic cleanup
- Prevents orphaned records
db.run("BEGIN TRANSACTION");
try {
db.run("INSERT ...");
db.run("UPDATE ...");
db.run("COMMIT");
} catch (error) {
db.run("ROLLBACK");
throw error;
}Benefits:
- ACID compliance
- Data consistency
- Error recovery
No network access:
- No cloud storage
- No API calls (except Git/GitHub CLI)
- No user tracking
- No analytics
Data location:
- npm:
~/.memory-journal/memory_journal.db - Docker:
./data/memory_journal.db(volume mount)
Optional and transparent:
create_entry({
content: "...",
auto_context: false, // Disable Git integration
});Git operations:
- Read-only (no commits)
- Local repository info only
- Optional GitHub CLI (user-controlled)
Data captured:
- Repository name and path
- Current branch
- Latest commit
- GitHub issues (if
ghCLI installed)
Not captured:
- File contents
- Uncommitted changes
- Remote repository data
- Credentials
// Soft delete (default)
delete_entry({ entry_id: 42, permanent: false });
// Sets deleted_at timestamp, entry remains in DB
// Permanent delete (explicit)
delete_entry({ entry_id: 42, permanent: true });
// Removes from database completelyBenefits:
- Accidental deletion recovery
- Audit trail
- No data loss
Soft-deleted entries:
- Excluded from searches
- Not visible in lists
- Still in database (for recovery)
Security handled at multiple levels:
- OAuth 2.1 (HTTP transport, opt-in) - RFC-compliant JWT validation with scope enforcement
-
Simple token auth (HTTP transport, via
MCP_AUTH_TOKEN) - Basic Bearer token verification - MCP client (stdio transport) - Desktop app security (Cursor IDE, Claude Desktop)
- User controls which servers run
npm installation:
# Database created with user permissions
chmod 600 ~/.memory-journal/memory_journal.dbDocker installation:
# Volume mount with appropriate permissions
mkdir -m 755 dataIf using team collaboration, the TEAM_DB_PATH dictates where the shared database is located.
Security Warning: Because TEAM_DB_PATH is accessed by multiple users or processes, it must be placed on a secure shared volume with strict access controls to prevent unauthorized data access or modification.
# Set secure permissions on the team database directory
chmod 700 /shared/team-dataDocker environment variables:
{
"args": [
"-e",
"DB_PATH=/app/data/custom.db"
// NO sensitive data in env vars
// NO API keys
// NO credentials
]
}No secrets required:
- No API keys
- No passwords
- No tokens
- Optional: GitHub CLI (user manages
gh auth)
1. Regular backups:
# Backup database
cp ./data/memory_journal.db ./backups/journal-$(date +%Y%m%d).db2. Docker image verification:
# Verify SHA digest
docker images --digests | grep memory-journal
# Use SHA-pinned images
docker pull writenotenow/memory-journal-mcp@sha256:...3. Limit Docker access:
{
"args": [
"-v",
"./data:/app/data", // Only mount data dir
"--read-only", // Read-only root filesystem
"--tmpfs",
"/tmp" // Writable tmp
]
}4. Review logs:
# Check for errors
docker logs <container_id>1. Input validation:
if (content.length > MAX_CONTENT_SIZE) {
throw new Error(`Content too large: ${content.length} > ${MAX_CONTENT_SIZE}`);
}2. Parameterized queries:
// Always use ? placeholders
db.run("SELECT * FROM memory_journal WHERE id = ?", [entryId]);3. Deterministic error handling:
Every tool handler uses formatHandlerErrorResponse() to guarantee structured responses:
import { formatHandlerErrorResponse } from "../utils/error-helpers.js";
try {
// Operation
return { success: true /* ... */ };
} catch (error) {
return formatHandlerErrorResponse(error, "tool_name");
// → { success: false, error: "tool_name failed: <message>", code: "...", category: "...", suggestion: "...", recoverable: true/false }
}Agents always receive { success, error } — no raw exceptions, no silent failures.
4. Least privilege:
// Don't request unnecessary permissions
// Don't access unnecessary files
// Don't make unnecessary network callsFound a security issue?
- Do NOT open a public GitHub issue
- Use GitHub Security Advisories to report privately
- Include:
- Description of vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We will:
- Acknowledge within 48 hours
- Provide fix timeline
- Credit you (if desired)
- Publish security advisory
- ✅ Input validation (Zod schemas + size limits)
- ✅ SQL injection prevention (parameterized queries)
- ✅ LIKE pattern sanitization (wildcard escaping)
- ✅ Path traversal protection (backup filenames)
- ✅ HTTP security headers (CSP, X-Frame-Options, Permissions-Policy, etc.)
- ✅ HTTP rate limiting (100 req/min)
- ✅ CORS configuration (configurable origin)
- ✅ Session timeout management (30 min)
- ✅ Token scrubbing in error logs
- ✅ OAuth 2.1 authentication (RFC 9728/8414, opt-in)
- ✅ JWT validation with JWKS caching
- ✅ Scope-based access control (read/write/admin)
- ✅ No credentials in code
- ✅ Non-root Docker execution
- ✅ Minimal base image (Alpine)
- ✅ Local-only data storage
- ✅ Transaction safety (ACID)
- ✅ Error handling (deterministic
{success, error, code, category, suggestion, recoverable}on every tool viaformatHandlerErrorResponse) - ✅ Soft delete option
Protected against:
- SQL injection
- Resource exhaustion (size limits)
- Accidental data loss (soft delete)
- Database integrity (transactions)
- Container escape (non-root, no privileges)
Not protected against:
- Malicious MCP client
- Compromised host system
- Physical access to database file
- User deleting database file
Why:
- Local-first design trusts the host
- User has full control
- Desktop application security model
GDPR-friendly:
- No data collection
- No data transmission
- Local storage only
- User controls all data
- Easy data export/deletion
No PII collection:
- No names
- No email addresses
- No tracking
- No analytics
npm:
# Update to latest version
npm install -g memory-journal-mcp@latestDocker:
# Pull latest image
docker pull writenotenow/memory-journal-mcp:latest
# Or specific SHA
docker pull writenotenow/memory-journal-mcp@sha256:...Monitor:
- GitHub releases
- Docker Hub updates
- Security advisories
Next: Check Architecture or Performance.