Sitelet https://github.com/codegiveness/postgresql-sharp-mcp/security
Skip to content

Security: codegiveness/postgresql-sharp-mcp

SECURITY.md

Security

Trust boundary

This server exposes PostgreSQL operations to an MCP client over stdio. Anyone who can use that client can request queries using the configured database credentials. There is no separate MCP user authentication, per-tool authorization, or tenant identity mechanism. Protect the host process and the client that launches it; do not expose stdio through an unauthenticated network bridge.

Connection profiles are case-sensitive names mapped to bootstrap connection strings, not a database allowlist. An inherited POSTGRES_CONNECTION_STRING alone creates the primary profile; protected targets JSON/file remains optional for multiple independent profiles. Tool requests can select a physical PostgreSQL database on a profile's server, but cannot supply a new connection string or change its host, login or TLS settings. Optional target selects the profile; without it, exact aliases select their configured bootstrap database and other database names use the default profile (primary, otherwise ordinal-first). With target, database is always a physical name. Unknown profiles, missing databases and denied connections fail without falling back. Each call owns its connection selection; there is no shared current database. Equivalent configurations for the same physical database may share a bounded connection pool.

Live list_databases uses the bootstrap database's catalog, excluding templates, disabled connections and databases lacking the current role's CONNECT privilege. It exposes accessible database names, not just configured aliases. CONNECT discovery does not prove actual connectivity or schema/table access. Newly created or granted databases are selectable without configuration edits or restarting, whether credentials came from the session environment or a protected file. Review grants, including PUBLIC CONNECT, before upgrading from alias-only selection. An optional POSTGRES_DATABASES/--databases allowlist with a base connection string filters listing and rejects other physical names; targets-file aliases do not impose that restriction.

PostgreSQL permissions are the authority

Use dedicated, least-privileged PostgreSQL roles, not superusers or database owners. Grant only the required database connection, schema usage, table access, and routine execution privileges. Configure row-level security and separate roles where required by the application's data model. Review role memberships, default privileges, PUBLIC grants, SECURITY DEFINER routines, extensions, foreign servers, and monitoring privileges.

Unrestricted access is the default when access mode is omitted. SQL requests still default to read_only=true; read-only operations use a server-owned read-only transaction and roll it back after reading the result. Writes require read_only=false on execute_sql, unrestricted mode and sufficient PostgreSQL privileges. Opt into POSTGRES_ACCESS_MODE=restricted or --access-mode restricted to refuse write requests; existing explicit restricted settings remain effective. Enabled writes use a transaction that commits after successful execution and result reading; failures dispose the transaction without committing. Other tools remain read-only. Connection loss or cancellation during commit can leave the caller uncertain whether a write committed; inspect database state before retrying. The server does not automatically replay writes.

The SQL guard is a statement-boundary lexer, not an authorization parser or SQL sandbox. It accepts one statement, handles quoted SQL and comments, and rejects direct transaction/session control plus unsupported operations such as COPY, DO, CALL, and VACUUM. PostgreSQL transactions and role permissions enforce the actual access restrictions.

Read-only transactions do not make arbitrary SQL harmless. Queries can execute functions, change session settings through functions, use advisory locks, access temporary objects, consume database resources, or cause external effects through installed extensions and privileged routines. EXPLAIN ANALYZE executes the query. Neither transaction rollback nor connection reset can undo external effects. Restrict these capabilities in PostgreSQL. HypoPG evaluation uses installed extension functions and connection-local hypothetical indexes; it does not grant permission to create permanent indexes or install extensions.

Credentials and diagnostics

The single-server quickstart uses masked Bash/Zsh/PowerShell entry into the process environment, not a credentials-bearing MCP JSON entry. Masking prevents terminal echo; the resulting value is still plaintext process state, inherited by child processes and inspectable by sufficiently privileged local processes. Process-level environment assignment does not automatically persist across sessions, but launchers, supervisors, containers or explicit persistence can retain it. Fully quit the client/server after changing a session credential, then launch the client from the same newly prepared shell. An already running client, a desktop launch or /mcp reload alone cannot acquire a changed parent-shell environment. Unsetting a parent variable does not erase copies already inherited by running children or deliberately persisted elsewhere.

An optional owner-protected targets file remains useful for multiple profiles, automation and clients where session propagation is impractical. It is persistent plaintext, protected by OS permissions rather than encryption; restrict file/folder access to the server's operating-system user and consider administrator, backup and sync exposure. A session environment is not inherently safer than an owner-only file: choose based on exposure, inheritance and lifecycle requirements. Keep existing protected file names if desired; no automatic local credential migration is performed.

Never put real credentials in committed files, SQL, tool arguments, profile aliases, literal CLI commands, shell history, shell startup profiles/rc files, setx or MCP JSON. A reusable rc function may contain only the masked-prompt code, invoked explicitly in the launching shell—not a literal secret or automatic plaintext export. CLI connection strings can be exposed in process arguments/history. Combining a connection string with targets JSON/file is rejected; when switching workflows, clear stale variables and remove conflicting client env/launch arguments rather than expecting secret precedence. Restart the server after changing file-based profiles/credentials; live database creation and grant/revoke changes need no recurring JSON edits.

The server does not return configured connection strings through database discovery. Live discovery can reveal sensitive database names; protect its results like other metadata. Configuration-parser failures use fixed diagnostics rather than forwarding parser or file-access exception messages. Unrecognized CLI arguments are not echoed. Missing or duplicate recognized options can identify the supported option name.

Npgsql error details and parameter logging are disabled by server-owned connection settings. PostgreSQL MessageText and Hint can contain query literals, data values, or arbitrary routine-generated text even when error details are disabled. Tool errors therefore retain SQLSTATE and a server-defined summary but do not forward these PostgreSQL fields. Connection and unexpected failures also use fixed summaries. Investigate detailed database errors using an appropriately protected PostgreSQL diagnostic channel.

MCP SDK and Npgsql logger categories are disabled, including when the configured log level is debug or trace, because SDK transport logging can include complete requests and responses. Remaining host diagnostics go to stderr; stdout is reserved for MCP JSON-RPC. The log-level setting does not enable SDK wire tracing.

These protections are not content redaction for successful results. Queries, plans, object definitions, statistics, workload text, and metadata can contain sensitive information that the configured role is allowed to read. --validate writes the actual database name and a bounded result or safe error to stderr. An MCP client, process supervisor, PostgreSQL server, proxy, or external diagnostic tool can independently record sensitive inputs and outputs. Protect those channels as well.

Connection and resource settings

TLS and certificate trust are operator-controlled Npgsql connection-string settings. The server does not force TLS or verify certificates on the operator's behalf. For remote production connections, configure certificate validation appropriate to the deployment, such as SSL Mode=VerifyFull, and provision the necessary trust material. Use network controls to restrict database reachability.

The server owns pool bounds, reset-on-close, transaction enlistment, multiplexing, application name, command timeouts, and logging safety settings. Configuration with No Reset On Close=true or multiplexing is rejected. Operation deadlines include queue and connection-pool wait; transactions also set statement and lock timeouts. Result row, byte, cell, and column limits bound responses. These are resource controls, not a guarantee against expensive queries, external side effects, or denial of service. Apply PostgreSQL and operating-system resource controls where needed.

Pools are keyed by normalized connection settings, including credentials/TLS and physical database. The runtime cache holds at most floor(256 / POSTGRES_POOL_SIZE) pool entries, each with that configured connection maximum. Idle least-recently-used entries are disposed when capacity is needed; active entries are not evicted. At full active capacity, calls wait within their existing operation deadline. Failed selections do not accumulate pool entries. These bounds prevent unbounded per-database pool growth, not privileged cross-database access.

Reporting a vulnerability

Do not post credentials, private data, or an exploit against an operational database in a public issue. Private vulnerability reporting is enabled for this repository: open Security and quality, then Report a vulnerability, following GitHub's private-report instructions. No particular response time is guaranteed. If the private route becomes unavailable, open a minimal public issue asking the maintainer for a confidential contact route, without sensitive details.

A useful report includes the affected version or commit, operating system and PostgreSQL version, configuration with secrets removed, the security boundary crossed, and a minimal reproduction using disposable data. Do not test systems without authorization. This document describes implementation protections and limitations; it is not an independent security audit or certification.

There aren't any published security advisories