Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@
the same 8-byte big-endian i64 wire format `INT8` uses and reuses the
existing `i64_to_json` JS-safe-integer stringification, matching the
builtin driver's `extract/advanced_types.rs::Money`.
- `ssl_mode=require`/`verify-ca`/`verify-full` silently allowed plaintext
connections — `build_pool` never called `cfg.ssl_mode(...)` on the
`deadpool_postgres::Config`, so the underlying `tokio_postgres::Config`
kept its own default (`SslMode::Prefer`: negotiate TLS if offered, but
accept plaintext otherwise) regardless of what this plugin's `ssl_mode`
was actually set to. A user opting into "TLS or nothing" got an
unencrypted connection with no error if the server couldn't/wouldn't
negotiate TLS — a security-relevant gap, not just a correctness one.
Added `resolve_ssl_mode`, mapping this plugin's `ssl_mode` strings to
`deadpool_postgres::SslMode` to match the builtin driver's
`build_postgres_configurations` mapping exactly (`disable`→`Disable`,
`allow`/`prefer`→`Prefer`, `require`/`verify-ca`/`verify-full`→`Require`),
and wired it into `build_pool`. Proved the bug and the fix with a live
test against a real non-SSL PostgreSQL instance: `ssl_mode=require`
connected successfully before this fix, and now correctly fails.

## [1.0.0-beta.7] - 2026-08-17

Expand Down
20 changes: 19 additions & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{LazyLock, Mutex};

use deadpool_postgres::{Config, ManagerConfig, Pool, RecyclingMethod, Runtime};
use deadpool_postgres::{Config, ManagerConfig, Pool, RecyclingMethod, Runtime, SslMode};
use tokio_postgres::types::{ToSql, Type};
use tokio_postgres::{NoTls, Row};
use tokio_postgres_rustls::MakeRustlsConnect;
Expand Down Expand Up @@ -311,6 +311,7 @@ async fn build_pool(params: &ConnectionParams) -> Result<Pool, String> {
cfg.manager = Some(ManagerConfig {
recycling_method: RecyclingMethod::Fast,
});
cfg.ssl_mode = resolve_ssl_mode(params.ssl_mode.as_deref());

let script = params
.startup_script
Expand Down Expand Up @@ -417,6 +418,23 @@ fn needs_tls(params: &ConnectionParams) -> bool {
)
}

/// Map this plugin's `ssl_mode` strings to `deadpool_postgres::SslMode`,
/// so `require`/`verify-ca`/`verify-full` actually force TLS at the protocol
/// level instead of leaving `tokio_postgres`'s own default (`SslMode::Prefer`)
/// in effect, which silently accepts a plaintext connection when the server
/// doesn't offer TLS. Matches the builtin driver's `ssl_mode` mapping in
/// `build_postgres_configurations` (`src-tauri/src/pool_manager.rs`) exactly.
/// Certificate/hostname verification is unaffected — that's handled
/// separately by `build_tls_connector`.
fn resolve_ssl_mode(ssl_mode: Option<&str>) -> Option<SslMode> {
match ssl_mode {
Some("disable") => Some(SslMode::Disable),
Some("allow" | "prefer") => Some(SslMode::Prefer),
Some("require" | "verify-ca" | "verify-full") => Some(SslMode::Require),
_ => None,
}
}

/// Build a rustls ClientConfig. `verify-ca`/`verify-full` validate the
/// server's certificate chain — against a caller-supplied CA bundle
/// (`ssl_ca`) when present, or the platform trust store otherwise.
Expand Down
38 changes: 37 additions & 1 deletion src/client_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ use tokio::sync::Mutex;

use super::{
build_tls_connector, cleanup_idle_pools, connection_key, get_or_create_pool,
load_client_cert_from_pem, load_roots_from_pem, VerifyCaCertVerifier, POOLS,
load_client_cert_from_pem, load_roots_from_pem, resolve_ssl_mode, VerifyCaCertVerifier, POOLS,
};
use crate::models::ConnectionParams;
use deadpool_postgres::SslMode;

// `POOLS` is a single process-wide static, and Rust's test harness runs
// `#[tokio::test]` fns concurrently on separate threads — without this,
Expand Down Expand Up @@ -592,3 +593,38 @@ fn build_tls_connector_verify_ca_attaches_a_configured_client_cert() {
"verify-ca must attach the configured client cert, not silently drop it"
);
}

// Coverage for #43: build_pool never called cfg.ssl_mode(...), so
// tokio_postgres's own default (SslMode::Prefer) applied regardless of
// ssl_mode=require/verify-ca/verify-full, letting connections silently
// fall back to plaintext instead of enforcing TLS at the protocol level.
// resolve_ssl_mode maps this plugin's ssl_mode strings to
// tokio_postgres::config::SslMode, matching the builtin driver's
// build_postgres_configurations mapping exactly.

#[test]
fn resolve_ssl_mode_maps_disable() {
assert_eq!(resolve_ssl_mode(Some("disable")), Some(SslMode::Disable));
}

#[test]
fn resolve_ssl_mode_maps_allow_and_prefer_to_prefer() {
assert_eq!(resolve_ssl_mode(Some("allow")), Some(SslMode::Prefer));
assert_eq!(resolve_ssl_mode(Some("prefer")), Some(SslMode::Prefer));
}

#[test]
fn resolve_ssl_mode_maps_require_verify_ca_and_verify_full_to_require() {
assert_eq!(resolve_ssl_mode(Some("require")), Some(SslMode::Require));
assert_eq!(resolve_ssl_mode(Some("verify-ca")), Some(SslMode::Require));
assert_eq!(
resolve_ssl_mode(Some("verify-full")),
Some(SslMode::Require)
);
}

#[test]
fn resolve_ssl_mode_leaves_unset_or_unknown_values_unmapped() {
assert_eq!(resolve_ssl_mode(None), None);
assert_eq!(resolve_ssl_mode(Some("bogus")), None);
}
20 changes: 20 additions & 0 deletions tests/live_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,23 @@ fn broken_startup_script_fails_fast_with_clear_attribution() {
"error should be clearly attributed to the startup script, got: {error}"
);
}

// Coverage for #43: build_pool never called cfg.ssl_mode(...), so
// tokio_postgres's own default (SslMode::Prefer) applied regardless of the
// plugin's ssl_mode value, letting ssl_mode=require silently connect over
// plaintext instead of failing. CI's live-db-integration fixture runs a
// plain `postgres:16` container with no SSL configured (see
// .github/workflows/ci.yml), so this must fail here just like it would
// against any server that hasn't been configured to offer TLS.
#[test]
fn ssl_mode_require_fails_against_a_server_without_tls() {
let mut plugin = Plugin::spawn();
let mut params = conn_params();
params["ssl_mode"] = json!("require");

let response = plugin.call("test_connection", json!({ "params": params }));
assert!(
response.get("error").is_some(),
"ssl_mode=require must fail against a server with no TLS, not silently connect over plaintext"
);
}
Loading