Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions deps/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ postgresql/postgresql/src/interfaces/libpq/libpq.a:
cd postgresql/postgresql && patch -p0 < ../bind_fmt_text.patch
cd postgresql/postgresql && patch -p0 < ../pqsendpipelinesync.patch
cd postgresql/postgresql && patch -p0 < ../sslkeylogfile.patch
cd postgresql/postgresql && patch -p0 < ../scram_verifier_auth.patch
ifeq ($(UNAME_S),Darwin)
cd postgresql/postgresql && LDFLAGS="-L$$(brew --prefix icu4c)/lib" CPPFLAGS="-I$$(brew --prefix icu4c)/include" PKG_CONFIG_PATH="$$(brew --prefix icu4c)/lib/pkgconfig:$$PKG_CONFIG_PATH" DYLD_LIBRARY_PATH="$(SSL_LDIR):$$DYLD_LIBRARY_PATH" ./configure --with-ssl=openssl --with-includes="$(SSL_IDIR)" --with-libraries="$(SSL_LDIR)" --without-readline --with-icu
else
Expand Down
205 changes: 205 additions & 0 deletions deps/postgresql/scram_verifier_auth.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
--- ../tmp/src/interfaces/libpq/libpq-int.h 2026-06-24 03:51:37.037786331 +0500
+++ ./src/interfaces/libpq/libpq-int.h 2026-06-24 03:03:14.441153729 +0500
@@ -384,6 +384,9 @@
char *pgpassfile; /* path to a file containing password(s) */
char *channel_binding; /* channel binding mode
* (require,prefer,disable) */
+ char *scram_client_key; /* base64 32-byte ClientKey (ProxySQL SCRAM pass-through) */
+ char *scram_server_key; /* base64 32-byte ServerKey (ProxySQL SCRAM pass-through) */
+ char *md5_secret; /* stored "md5"+32hex to reuse for backend md5 auth */
char *keepalives; /* use TCP keepalives? */
char *keepalives_idle; /* time between TCP keepalives */
char *keepalives_interval; /* time between TCP keepalive
--- ../tmp/src/interfaces/libpq/fe-connect.c 2026-06-24 03:51:37.040811504 +0500
+++ ./src/interfaces/libpq/fe-connect.c 2026-06-24 03:04:47.178883449 +0500
@@ -361,6 +361,18 @@
"Load-Balance-Hosts", "", 8, /* sizeof("disable") = 8 */
offsetof(struct pg_conn, load_balance_hosts)},

+ {"scram_client_key", NULL, NULL, NULL,
+ "SCRAM-Client-Key", "*", 64,
+ offsetof(struct pg_conn, scram_client_key)},
+
+ {"scram_server_key", NULL, NULL, NULL,
+ "SCRAM-Server-Key", "*", 64,
+ offsetof(struct pg_conn, scram_server_key)},
+
+ {"md5_secret", NULL, NULL, NULL,
+ "MD5-Secret", "*", 64,
+ offsetof(struct pg_conn, md5_secret)},
+
/* Terminating entry --- MUST BE LAST */
{NULL, NULL, NULL, NULL,
NULL, NULL, 0}
@@ -4436,6 +4436,22 @@
}
free(conn->pgpassfile);
free(conn->channel_binding);
+ /* ProxySQL SCRAM/md5 verifier pass-through: free + scrub the injected key material. */
+ if (conn->scram_client_key)
+ {
+ explicit_bzero(conn->scram_client_key, strlen(conn->scram_client_key));
+ free(conn->scram_client_key);
+ }
+ if (conn->scram_server_key)
+ {
+ explicit_bzero(conn->scram_server_key, strlen(conn->scram_server_key));
+ free(conn->scram_server_key);
+ }
+ if (conn->md5_secret)
+ {
+ explicit_bzero(conn->md5_secret, strlen(conn->md5_secret));
+ free(conn->md5_secret);
+ }
free(conn->keepalives);
free(conn->keepalives_idle);
free(conn->keepalives_interval);
--- ../tmp/src/interfaces/libpq/fe-auth-scram.c 2026-06-24 03:51:37.041568997 +0500
+++ ./src/interfaces/libpq/fe-auth-scram.c 2026-06-24 03:08:23.927939654 +0500
@@ -120,6 +120,29 @@
return NULL;
}

+ /*
+ * ProxySQL SCRAM pass-through: when a ClientKey is injected the exchange
+ * uses it instead of a password. Require BOTH ClientKey and ServerKey (or
+ * neither), so mutual authentication can never be silently skipped.
+ */
+ {
+ bool has_ck = (conn->scram_client_key && conn->scram_client_key[0]);
+ bool has_sk = (conn->scram_server_key && conn->scram_server_key[0]);
+
+ if (has_ck != has_sk)
+ {
+ free(state->sasl_mechanism);
+ free(state);
+ return NULL;
+ }
+ if (has_ck)
+ {
+ /* No password to normalize; keys are injected. */
+ state->password = NULL;
+ return state;
+ }
+ }
+
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
if (rc == SASLPREP_OOM)
@@ -785,14 +808,37 @@
* Calculate SaltedPassword, and store it in 'state' so that we can reuse
* it later in verify_server_signature.
*/
- if (scram_SaltedPassword(state->password, state->hash_type,
- state->key_length, state->salt, state->saltlen,
- state->iterations, state->SaltedPassword,
- errstr) < 0 ||
- scram_ClientKey(state->SaltedPassword, state->hash_type,
- state->key_length, ClientKey, errstr) < 0 ||
- scram_H(ClientKey, state->hash_type, state->key_length,
- StoredKey, errstr) < 0)
+ if (state->conn->scram_client_key && state->conn->scram_client_key[0])
+ {
+ /*
+ * ProxySQL SCRAM pass-through: use the injected ClientKey directly and
+ * derive StoredKey = SHA256(ClientKey). Skips SASLprep + PBKDF2.
+ */
+ int dec = pg_b64_decode(state->conn->scram_client_key,
+ strlen(state->conn->scram_client_key),
+ (char *) ClientKey, state->key_length);
+
+ if (dec != state->key_length)
+ {
+ *errstr = "invalid scram_client_key";
+ pg_hmac_free(ctx);
+ return false;
+ }
+ if (scram_H(ClientKey, state->hash_type, state->key_length,
+ StoredKey, errstr) < 0)
+ {
+ pg_hmac_free(ctx);
+ return false;
+ }
+ }
+ else if (scram_SaltedPassword(state->password, state->hash_type,
+ state->key_length, state->salt, state->saltlen,
+ state->iterations, state->SaltedPassword,
+ errstr) < 0 ||
+ scram_ClientKey(state->SaltedPassword, state->hash_type,
+ state->key_length, ClientKey, errstr) < 0 ||
+ scram_H(ClientKey, state->hash_type, state->key_length,
+ StoredKey, errstr) < 0)
{
/* errstr is already filled here */
pg_hmac_free(ctx);
@@ -847,8 +893,22 @@
return false;
}

- if (scram_ServerKey(state->SaltedPassword, state->hash_type,
- state->key_length, ServerKey, errstr) < 0)
+ if (state->conn->scram_server_key && state->conn->scram_server_key[0])
+ {
+ /* ProxySQL SCRAM pass-through: verify with the injected ServerKey. */
+ int dec = pg_b64_decode(state->conn->scram_server_key,
+ strlen(state->conn->scram_server_key),
+ (char *) ServerKey, state->key_length);
+
+ if (dec != state->key_length)
+ {
+ *errstr = "invalid scram_server_key";
+ pg_hmac_free(ctx);
+ return false;
+ }
+ }
+ else if (scram_ServerKey(state->SaltedPassword, state->hash_type,
+ state->key_length, ServerKey, errstr) < 0)
{
/* errstr is filled already */
pg_hmac_free(ctx);
--- ../tmp/src/interfaces/libpq/fe-auth.c 2026-06-24 03:51:37.042103163 +0500
+++ ./src/interfaces/libpq/fe-auth.c 2026-06-24 03:08:34.430125390 +0500
@@ -553,7 +553,8 @@
password = conn->connhost[conn->whichhost].password;
if (password == NULL)
password = conn->pgpass;
- if (password == NULL || password[0] == '\0')
+ if ((password == NULL || password[0] == '\0') &&
+ !(conn->scram_client_key && conn->scram_client_key[0]))
{
appendPQExpBufferStr(&conn->errorMessage,
PQnoPasswordSupplied);
@@ -731,9 +732,20 @@
}

crypt_pwd2 = crypt_pwd + MD5_PASSWD_LEN + 1;
- if (!pg_md5_encrypt(password, conn->pguser,
- strlen(conn->pguser), crypt_pwd2,
- &errstr))
+ if (conn->md5_secret && conn->md5_secret[0])
+ {
+ /* ProxySQL: reuse the stored md5 secret as the inner hash. */
+ if (strlen(conn->md5_secret) != MD5_PASSWD_LEN)
+ {
+ libpq_append_conn_error(conn, "invalid md5_secret");
+ free(crypt_pwd);
+ return STATUS_ERROR;
+ }
+ strcpy(crypt_pwd2, conn->md5_secret);
+ }
+ else if (!pg_md5_encrypt(password, conn->pguser,
+ strlen(conn->pguser), crypt_pwd2,
+ &errstr))
{
libpq_append_conn_error(conn, "could not encrypt password: %s", errstr);
free(crypt_pwd);
@@ -1096,7 +1108,8 @@
password = conn->connhost[conn->whichhost].password;
if (password == NULL)
password = conn->pgpass;
- if (password == NULL || password[0] == '\0')
+ if ((password == NULL || password[0] == '\0') &&
+ !(conn->md5_secret && conn->md5_secret[0]))
{
appendPQExpBufferStr(&conn->errorMessage,
PQnoPasswordSupplied);
21 changes: 19 additions & 2 deletions include/PgSQL_Connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@
void fill_client_internal_session(nlohmann::json &j, int idx);
};

// Length of a SCRAM-SHA-256 ClientKey/ServerKey (== SHA256_DIGEST_LENGTH).
#define PGSQL_SCRAM_KEY_LEN 32

Check failure on line 228 in include/PgSQL_Connection.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this macro by "const", "constexpr" or an "enum".

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ9vC104a242REQ82Rhu&open=AZ9vC104a242REQ82Rhu&pullRequest=5865

class PgSQL_Connection_userinfo {
private:
uint64_t compute_hash();
Expand All @@ -237,7 +240,12 @@
};
char *sha1_pass;
char *fe_username;
// TODO POSGRESQL: add client and server scram keys
// ClientKey harvested from the client's frontend login + the stored verifier's ServerKey,
// carried to the backend connection. Deliberately NOT part of compute_hash() so connection-pool
// reuse semantics are unchanged.
uint8_t scram_client_key[PGSQL_SCRAM_KEY_LEN];
uint8_t scram_server_key[PGSQL_SCRAM_KEY_LEN];
bool has_scram_keys;
Comment on lines +243 to +248

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Invalidate pooled backends when SCRAM credentials rotate.

Preserving the existing reuse semantics leaves the confirmed case where an A-era backend connection can serve after verifier B is loaded. Include a credential generation/fingerprint in pool matching and evict mismatched idle connections during credential reload; otherwise rotation or revocation does not take effect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/PgSQL_Connection.h` around lines 243 - 248, Update the SCRAM
credential state associated with scram_client_key, scram_server_key, and
has_scram_keys to include a credential generation or fingerprint used during
pool matching. During credential reload, advance or replace that identifier and
evict idle pooled backends whose stored identifier no longer matches, while
preserving reuse for connections using the current credentials.

PgSQL_Connection_userinfo();
~PgSQL_Connection_userinfo();
void set(char *, char *, char *, char *);
Expand Down Expand Up @@ -512,7 +520,7 @@
const char* get_pg_transaction_status_str();
unsigned int get_memory_usage() const;
char get_transaction_status_char();
inline int get_backend_pid() { return (pgsql_conn) ? get_pg_backend_pid() : -1; }

Check warning on line 523 in include/PgSQL_Connection.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove these redundant parentheses.

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ9vC104a242REQ82Rhv&open=AZ9vC104a242REQ82Rhv&pullRequest=5865
bool is_pipeline_active() { return (PQpipelineStatus(pgsql_conn) != PQ_PIPELINE_OFF); }
const char* get_pg_backend_state() const;

Expand Down Expand Up @@ -722,6 +730,13 @@
char* dbname;
unsigned int port;

// Copies of the credential material harvested for this user's frontend login. The kill/terminate
// connection authenticates to the backend exactly like a pooled one, so it needs the same
// pass-through keys: a verifier/md5 secret shipped as a plaintext 'password' is rejected.
uint8_t scram_client_key[PGSQL_SCRAM_KEY_LEN];
uint8_t scram_server_key[PGSQL_SCRAM_KEY_LEN];
bool has_scram_keys;

int backend_pid;
unsigned int hostgroup_id;
TYPE type;
Expand All @@ -738,7 +753,9 @@
char* ssl_max_protocol_version;
} ssl_config;

PgSQL_Backend_Kill_Args(PGconn* conn, const char* user, const char* pass, const char* db, const char* host,
// 'ui' supplies the credentials (username/password/dbname AND any harvested SCRAM keys); it is
// deep-copied, since the kill runs on a detached thread that outlives the source connection.
PgSQL_Backend_Kill_Args(PGconn* conn, const PgSQL_Connection_userinfo* ui, const char* host,
unsigned int port, unsigned int hid, bool ssl, TYPE typ, PgSQL_Thread* thd);
~PgSQL_Backend_Kill_Args();
};
Expand Down
5 changes: 5 additions & 0 deletions include/PgSQL_Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
class ProxySQL_Admin;
struct PgCredentials;
struct ScramState;
// Auth-method selection: map the floor (pgsql-authentication_method;
// 1=cleartext, 2=md5, 3=scram) + the user's stored secret type (a PasswordType, as int) to the
// AUTHENTICATION_METHOD to challenge with (as int); *reject=true when the stored secret is too weak
// for the floor (caller runs the generic mock-fail). Defined in PgSQL_Protocol.cpp.
int pgsql_reconcile_auth_method(int floor, int stored, bool* reject);

enum class EXECUTION_STATE {
FAILED = 0,
Expand Down
9 changes: 9 additions & 0 deletions lib/PgSQL_Authentication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ using json = nlohmann::json;
#include "proxysql_atomic.h"

#include "PgSQL_Authentication.h"
#include "scram.h" // get_password_type, PasswordType (load-time credential validation)

#ifndef SPOOKYV2
#include "SpookyV2.h"
Expand Down Expand Up @@ -87,6 +88,14 @@ void PgSQL_Authentication::remove_inactives(enum cred_username_type usertype) {
}

bool PgSQL_Authentication::add(char * username, char * password, enum cred_username_type usertype, bool use_ssl, int default_hostgroup, bool transaction_persistent, bool fast_forward, int max_connections, char* attributes, char *comment) {
// Reject a credential that looks like a SCRAM verifier but does not parse, so a
// mistyped verifier is never silently stored as a literal plaintext password. (md5 follows the
// PostgreSQL convention: "md5"+32hex is md5, anything else is plaintext — so no md5 rejection here.)
if (password && strncmp(password, "SCRAM-SHA-256$", 14) == 0
&& get_password_type(password) != PASSWORD_TYPE_SCRAM_SHA_256) {
proxy_error("pgsql_users: user '%s' has a malformed SCRAM-SHA-256 verifier; skipping\n", username);
return false;
}
uint64_t hash1, hash2;
SpookyHash myhash;
myhash.Init(1,2);
Expand Down
Loading
Loading