- Security Model
- Threat Model
- Capability Enforcement
- Credential Security
- SQL Injection & Query Validation
- Known Security Issues
- Security Recommendations
- Reporting Security Vulnerabilities
Plenum's security boundary is capability-based access control:
- ✅ Operation type restrictions (read-only, write, DDL)
- ✅ Row limits (
max_rows) - ✅ Query timeouts (
timeout_ms) - ✅ Pre-execution validation (no capability bypasses)
Plenum is designed as a constrained execution layer, not a security sandbox:
- ❌ SQL injection prevention (agent's responsibility)
- ❌ Query semantic correctness
- ❌ Business logic constraints
- ❌ Data access policies (row-level security, column masking)
- ❌ Rate limiting
- ❌ Audit logging
Design Principle: Plenum assumes SQL passed to it is safe. It provides capability constraints, not query validation.
- Trusted Agent: The calling AI agent is assumed to be trustworthy
- Untrusted User Input: User inputs must be sanitized by the agent before constructing SQL
- Local Machine Security: The host machine is assumed to be secured at the OS level
- Single-User Environment: Plenum is designed for development/automation, not multi-tenant production
In Scope:
- Capability bypass attempts
- Privilege escalation via SQL (mitigated by capability enforcement)
- Resource exhaustion (mitigated by
max_rowsandtimeout_ms)
Out of Scope:
- SQL injection via user inputs (agent must sanitize)
- Credential theft from config files (OS-level protection required)
- Side-channel attacks
- Network-level attacks (MCP over stdio only)
┌─────────────────────────────────────────┐
│ DDL (--allow-ddl) │
│ - CREATE, DROP, ALTER, TRUNCATE │
│ - Implicitly grants write permissions │
│ │
│ ┌───────────────────────────────────┐ │
│ │ Write (--allow-write) │ │
│ │ - INSERT, UPDATE, DELETE │ │
│ │ - Does NOT enable DDL │ │
│ │ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Read-Only (default) │ │ │
│ │ │ - SELECT only │ │ │
│ │ │ - No flags required │ │ │
│ │ └─────────────────────────────┘ │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
Capability validation occurs at two entry points:
- CLI (
src/main.rs:782): Validates before calling engine - MCP Server (
src/mcp.rs:485): Validates before calling engine
Every query execution path calls validate_query() before database interaction:
- SQLite (
src/engine/sqlite/mod.rs:138) - PostgreSQL (
src/engine/postgres/mod.rs:145) - MySQL (
src/engine/mysql/mod.rs:165)
No bypass paths exist. Capability enforcement has been audited and verified complete.
User SQL → validate_query() → Categorize → Check Capabilities → Engine
↓
(Read-Only/Write/DDL)
↓
Capability Check (fail-fast)
Credentials are stored as plaintext JSON:
- Local:
.plenum/config.json(team-shareable, project-specific) - Global:
~/.config/plenum/connections.json(user-private)
Security Responsibility: The user is responsible for securing these files at the OS level (file permissions, disk encryption, etc.).
For production use, passwords can be stored in environment variables:
{
"connections": {
"prod": {
"engine": "postgres",
"host": "db.example.com",
"port": 5432,
"user": "app_user",
"password_env": "DB_PASSWORD"
}
}
}Warning: Passwords passed via --password flag are visible in:
- Process listings (
ps aux) - Shell history
- System logs
Recommendation: Use password_env for automation, or interactive prompts for manual use.
When using the MCP server, credentials are passed per-invocation via JSON-RPC:
{
"name": "query",
"arguments": {
"sql": "SELECT * FROM users",
"engine": "postgres",
"host": "localhost",
"port": 5432,
"user": "app_user",
"password": "secret"
}
}Security Note: MCP communication over stdio is local-only (no network exposure).
Plenum does NOT validate SQL for safety. SQL is passed verbatim to database drivers.
The calling agent MUST:
- Sanitize all user inputs before constructing SQL
- Use parameterized queries where possible
- Validate query semantics before passing to Plenum
- Apply business logic constraints
Plenum's validate_query() function:
- ✅ Categorizes queries (read-only/write/DDL)
- ✅ Enforces capability constraints
- ❌ Does NOT sanitize SQL
- ❌ Does NOT prevent SQL injection
- ❌ Does NOT modify SQL (passed verbatim to drivers)
Example (unsafe agent code):
// UNSAFE: User input directly in SQL
let sql = format!("SELECT * FROM users WHERE name = '{}'", user_input);
plenum query --sql "$sql"Example (safe agent code):
// SAFE: Use database-specific parameterized queries
let sql = "SELECT * FROM users WHERE name = $1";
// Then sanitize/validate before calling Plenum1. Interactive Password Not Hidden
Location: src/main.rs:503-506
Issue: Interactive password prompt uses .interact_text() instead of .interact_password(), causing passwords to echo to the screen.
Impact: Passwords visible in terminal, screen recordings, shoulder surfing.
Mitigation: Use CLI flag with environment variable, or fix by using .interact_password().
Status: Identified, pending fix.
Location: src/engine/sqlite/mod.rs:49, 89, 147
Issue: Code uses file_path.to_str().unwrap() which panics if the file path contains non-UTF-8 characters.
let conn = open_connection(file_path.to_str().unwrap(), true)?; // PANICS on non-UTF-8Impact: CLI crashes on Windows file paths with special characters, emoji, or certain Unicode characters.
Mitigation: Avoid non-UTF-8 file paths, or fix by handling Option<&str> properly.
Status: Identified, pending fix.
Location: src/engine/postgres/mod.rs:56, 125, 158
Issue: PostgreSQL driver errors are logged to stderr via eprintln!() and can contain connection strings with credentials.
eprintln!("PostgreSQL connection error: {}", e);Impact: Credentials may appear in stderr output, logs, or terminal scrollback.
Mitigation: Sanitize error messages before logging, or disable stderr output.
Status: Identified, pending fix.
Location: All three engines (postgres/mysql/sqlite)
Issue: Database driver errors are wrapped with format!("Failed to connect: {}", e) and returned in JSON output. Driver errors can contain:
- Connection strings with passwords
- Host/port information
- Database/user names
Example:
{
"ok": false,
"error": {
"code": "CONNECTION_FAILED",
"message": "Failed to connect to PostgreSQL: FATAL: password authentication failed for user 'admin' (connection: 'postgresql://admin:SECRET@host:5432/db')"
}
}Impact: Credentials exposed in JSON error output (stdout, logs, MCP responses).
Mitigation: Sanitize driver errors to remove connection details before wrapping in PlenumError.
Status: Identified, pending fix.
Location: src/main.rs:851
Issue: MCP server errors are logged to stderr via eprintln!() without sanitization.
eprintln!("MCP server error: {}", e);Impact: Errors from MCP tools (including credential-related errors) may leak to stderr.
Mitigation: Sanitize errors before logging.
Status: Identified, pending fix.
Location: src/config/mod.rs:235
Issue: Connection resolution errors are logged to stderr and may contain credential information.
eprintln!("Warning: Could not resolve connection '{}': {}", name, e.message());Impact: Environment variable resolution errors or config parsing errors may expose credential metadata.
Mitigation: Sanitize error messages before logging.
Status: Identified, pending fix.
Location: src/engine/sqlite/mod.rs:279-280, src/engine/postgres/mod.rs:399, src/engine/mysql/mod.rs:431
Issue: Code uses HashMap::get_mut().unwrap() after or_insert_with(), which is logically safe but fragile and unclear to the compiler.
fk_map.entry(id).or_insert_with(|| (ref_table.clone(), Vec::new(), Vec::new()));
fk_map.get_mut(&id).unwrap().1.push(from_col); // FragileImpact: Code is hard to refactor and may panic if HashMap implementation changes.
Mitigation: Use Entry API pattern or pattern matching instead of unwrap().
Status: Identified, low priority.
-
Use Environment Variables for Production:
{ "connections": { "prod": { "password_env": "DB_PASSWORD" } } } -
Secure Config Files:
chmod 600 ~/.config/plenum/connections.json chmod 600 .plenum/config.json -
Use Read-Only by Default:
plenum query --sql "SELECT * FROM users" # Safe (read-only) plenum query --sql "DELETE FROM users" --allow-write # Explicit
-
Avoid CLI Passwords:
# Bad (visible in ps/history) plenum query --password "secret" --sql "..." # Good (environment variable) export DB_PASSWORD="secret" plenum query --password-env DB_PASSWORD --sql "..."
-
Limit Exposure:
- Use
--max-rowsfor unknown queries - Use
--timeout-msto prevent long-running operations - Start with read-only, escalate only when needed
- Use
-
Sanitize All User Inputs:
# UNSAFE sql = f"SELECT * FROM users WHERE name = '{user_input}'" # SAFE sql = "SELECT * FROM users WHERE name = $1" # PostgreSQL params = [user_input] # Pass separately, let agent validate
-
Use Least Privilege:
# Default: read-only result = plenum.query(sql="SELECT * FROM users") # Explicit escalation result = plenum.query(sql="INSERT INTO logs ...", allow_write=True)
-
Validate Before Execution:
# Check query intent before calling Plenum if is_destructive(sql): confirm_with_user() result = plenum.query(sql=sql, allow_write=True)
-
Handle Errors Securely:
try: result = plenum.query(...) except PlenumError as e: # Don't log raw error messages (may contain credentials) log.error(f"Query failed with error code: {e.code}")
If you discover a security vulnerability in Plenum, please report it responsibly:
Contact: Create a GitHub Issue with the security label
Information to Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if applicable)
Response Timeline:
- Acknowledgment: Within 48 hours
- Initial Assessment: Within 1 week
- Fix Timeline: Depends on severity (critical: <7 days, high: <30 days)
Capability Enforcement: ✅ Comprehensive
- 25+ tests in
src/capability/mod.rs - All capability bypass scenarios covered
- Engine-specific validation tests
Panic Safety: 🔄 In Progress
- Unwrap/expect usage audited
- Known panic risks identified
- Fixes pending
Credential Handling:
- Credential storage tested
- Environment variable resolution tested
- Leakage prevention NOT tested
SQL Injection: ❌ Not Applicable
- Plenum does not validate SQL safety
- Agent's responsibility to test
# All tests
cargo test --all-features
# Capability tests only
cargo test --lib capability
# Integration tests (requires database servers)
cargo test --features postgres,mysql -- --ignoredCompleted:
- ✅ Capability enforcement audit (no bypass paths found)
- ✅ Panic safety audit (7 issues identified)
- ✅ Credential handling audit (5 issues identified)
- ✅ Error message disclosure audit (4 issues identified)
- ✅ SQL verbatim verification (confirmed)
Findings:
- 2 CRITICAL issues (interactive password, SQLite panic)
- 3 HIGH issues (PostgreSQL leakage, driver error leakage, MCP leakage)
- 2 MEDIUM issues (config error leakage, HashMap unwrap fragility)
Next Steps:
- Fix identified security issues
- Add credential leakage tests
- Implement error message sanitization
- Add security section to README
Last Updated: 2025-01-08 Security Audit Status: Phase 8 Complete, Fixes Pending