Skip to content

Security and Best Practices

Temp edited this page Oct 3, 2025 · 1 revision

Security and Best Practices

Enterprise-grade security practices for PostgreSQL MCP Server.


🛡️ Security Overview

The PostgreSQL MCP Server is designed with security first:

  • Zero Known Vulnerabilities - Comprehensive security audit passed
  • SQL Injection Prevention - Automatic parameter binding validation
  • Dual Security Modes - Restricted (production) and unrestricted (development)
  • Type Safety - Pyright strict mode with LiteralString enforcement

🔒 Security Modes

Restricted Mode (Production)

Always use restricted mode for production databases.

postgres-mcp --access-mode=restricted

Protection Features:

  • ✅ Read-only operations only
  • ✅ Advanced SQL validation
  • ✅ Query timeout protection
  • ✅ Parameter binding required
  • ✅ Resource usage limits
  • ✅ DDL operations blocked

Allowed Operations:

  • SELECT queries with parameter binding
  • EXPLAIN and EXPLAIN ANALYZE
  • pg_stat_statements queries
  • Index analysis and recommendations

Blocked Operations:

  • INSERT, UPDATE, DELETE
  • DROP, CREATE, ALTER
  • User-defined functions
  • COPY commands
  • System catalog modifications

Unrestricted Mode (Development)

Use only in development/testing environments.

postgres-mcp --access-mode=unrestricted

Features:

  • ⚠️ Full read/write access
  • ✅ Parameter binding protection maintained
  • ⚠️ DDL operations allowed
  • ⚠️ No query restrictions

When to Use:

  • Local development databases
  • Testing environments
  • Staging databases (with caution)
  • Database migrations

When NOT to Use:

  • Production databases
  • Databases with sensitive data
  • Shared environments
  • Client-accessible systems

🔐 SQL Injection Prevention

Parameter Binding (Required)

All user input must use parameter binding:

# ✅ CORRECT: Using parameter binding execute_sql( sql="SELECT * FROM users WHERE id = %s", params=[user_id] ) # ❌ WRONG: String concatenation execute_sql( sql=f"SELECT * FROM users WHERE id = {user_id}" )

Automatic Validation

The server automatically validates all SQL queries:

# This will be REJECTED execute_sql( sql="SELECT * FROM users WHERE name = '" + user_input + "'" ) # Error: Potential SQL injection detected

Detection Patterns:

  • String concatenation in WHERE clauses
  • Dynamic table/column names without validation
  • Suspicious patterns (;, --, /*)
  • Multiple statements

Safe Dynamic Identifiers

For dynamic table/column names, use f-strings with validation:

# ✅ CORRECT: Validated identifier with parameterized values table_name = "users" # Validated/trusted source execute_sql( sql=f"SELECT * FROM {table_name} WHERE id = %s", params=[user_id] ) # ❌ WRONG: User input as identifier execute_sql( sql=f"SELECT * FROM {user_table} WHERE id = %s", # user_table from user input params=[user_id] )

🔑 Authentication & Authorization

Connection Security

Always use secure connection strings:

# ✅ GOOD: SSL/TLS required export DATABASE_URI="postgresql://user:pass@host:5432/db?sslmode=require" # ⚠️ WARNING: Unencrypted connection export DATABASE_URI="postgresql://user:pass@host:5432/db"

SSL Modes:

  • disable - No SSL (insecure)
  • allow - Try SSL, fall back to plain
  • prefer - Try SSL first (default)
  • require - Require SSL, fail if unavailable
  • verify-ca - Require SSL with CA verification
  • verify-full - Require SSL with hostname verification

Database User Permissions

Use principle of least privilege:

-- Read-only user for restricted mode CREATE USER mcp_readonly WITH PASSWORD 'secure_password'; GRANT CONNECT ON DATABASE mydb TO mcp_readonly; GRANT USAGE ON SCHEMA public TO mcp_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly; -- Required extensions GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO mcp_readonly;

Password Security

Never hardcode passwords:

# ❌ WRONG: Password in config file { "DATABASE_URI": "postgresql://user:password123@host:5432/db" } # ✅ CORRECT: Use environment variables export DATABASE_URI="postgresql://user:${DB_PASSWORD}@host:5432/db" # ✅ BETTER: Use secrets management export DATABASE_URI=$(vault kv get -field=uri database/postgres)

🚨 Query Safety

Timeout Protection

Set query timeouts to prevent runaway queries:

# In postgresql.conf statement_timeout = 30000 # 30 seconds # Per connection export DATABASE_URI="postgresql://user:pass@host:5432/db?options=-c statement_timeout=30s"

Resource Limits

Configure resource limits:

-- Limit memory per query SET work_mem = '64MB'; -- Limit temporary files SET temp_file_limit = '1GB'; -- Limit locks SET max_locks_per_transaction = 64;

Query Complexity

Monitor query complexity:

# Use explain_query to check cost before execution plan = explain_query( sql="SELECT * FROM large_table WHERE complex_condition = %s", params=[value], analyze=False # Plan only, don't execute ) if plan["total_cost"] > 10000: print("Warning: High-cost query")

📊 Audit Logging

Enable pg_stat_statements

-- In postgresql.conf shared_preload_libraries = 'pg_stat_statements' pg_stat_statements.track = 'all' pg_stat_statements.max = 10000

Track all queries for auditing:

# Review executed queries queries = get_top_queries(sort_by="calls", limit=100)

Application-Level Logging

Log all MCP operations:

import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', handlers=[ logging.FileHandler('mcp_audit.log'), logging.StreamHandler() ] ) # Operations are automatically logged

🔍 Security Monitoring

Regular Security Checks

# 1. Check for suspicious queries top_queries = get_top_queries(sort_by="calls", limit=50) # Review for unexpected patterns # 2. Monitor connection counts metrics = monitor_real_time(include_queries=True) # Alert on unusual connection spikes # 3. Scan JSON for security issues security = json_security_scan( table_name="user_data", json_column="profile" )

Alert on Anomalies

# Monitor for suspicious activity alert = alert_threshold_set( metric_type="connection_count", warning_threshold=80, critical_threshold=95, check_current=True ) if alert["alert_status"] == "critical": # Trigger alert print("ALERT: High connection count")

📚 Best Practices Checklist

Deployment

  • Use restricted mode in production
  • Enable SSL/TLS for connections
  • Use read-only database user
  • Set up connection pooling
  • Configure query timeouts
  • Enable audit logging
  • Use secrets management for credentials

Development

  • Always use parameter binding
  • Validate dynamic identifiers
  • Test with restricted mode before deployment
  • Review query plans for complex queries
  • Use hypothetical indexes before creating
  • Document security exceptions

Operations

  • Monitor query performance daily
  • Review audit logs weekly
  • Test disaster recovery procedures
  • Keep extensions updated
  • Rotate credentials regularly
  • Monitor for security advisories

🚧 Common Security Pitfalls

1. Trusting User Input

# ❌ NEVER do this table = user_input # User provides table name execute_sql(f"SELECT * FROM {table}") # ✅ Use whitelist validation ALLOWED_TABLES = ['users', 'orders', 'products'] if table in ALLOWED_TABLES: execute_sql(f"SELECT * FROM {table}")

2. Exposing Sensitive Data

# ❌ Don't expose passwords execute_sql("SELECT * FROM users") # Contains password hashes # ✅ Select only needed columns execute_sql("SELECT id, username, email FROM users")

3. Missing Access Controls

# ❌ No authorization check execute_sql( "SELECT * FROM orders WHERE id = %s", params=[order_id] ) # ✅ Verify user owns the resource execute_sql( "SELECT * FROM orders WHERE id = %s AND user_id = %s", params=[order_id, current_user_id] )

📖 Security Resources


🆘 Security Incident Response

If you discover a security vulnerability:

  1. DO NOT open a public issue
  2. Email: admin@adamic.tech
  3. Include:
    • Description of vulnerability
    • Steps to reproduce
    • Potential impact
    • Suggested fix (if any)

See SECURITY.md for details.


📚 Related Documentation


See Home for more tool categories.

Clone this wiki locally