brianc/node-postgres · critical · Error
SASL: SCRAM-SERVER-FIRST-MESSAGE: salt must be base64
Error message
SASL: SCRAM-SERVER-FIRST-MESSAGE: salt must be base64
What it means
Thrown by parseServerFirstMessage during SCRAM-SHA-256 authentication when the salt attribute (`s=`) from the server's first SASL message fails the base64 format check at sasl.js:204 (regex isBase64 at line 172). A compliant PostgreSQL server always emits a valid base64 salt, so this almost always indicates a corrupted or non-conformant server response rather than a client misconfiguration.
Source
Thrown at packages/pg/lib/crypto/sasl.js:205
return [name, value]
})
)
}
function parseServerFirstMessage(data) {
const attrPairs = parseAttributePairs(data)
const nonce = attrPairs.get('r')
if (!nonce) {
throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce missing')
} else if (!isPrintableChars(nonce)) {
throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce must only contain printable characters')
}
const salt = attrPairs.get('s')
if (!salt) {
throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: salt missing')
} else if (!isBase64(salt)) {
throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: salt must be base64')
}
const iterationText = attrPairs.get('i')
if (!iterationText) {
throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration missing')
} else if (!/^[1-9][0-9]*$/.test(iterationText)) {
throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: invalid iteration count')
}
const iteration = parseInt(iterationText, 10)
return {
nonce,
salt,
iteration,
}
}
function parseServerFinalMessage(serverData) {
const attrPairs = parseAttributePairs(serverData)View on GitHub (pinned to c5e8c9a57b)
Solutions
- Connect directly to the PostgreSQL primary (bypass the pooler/proxy) to isolate the mangling source.
- Upgrade PgBouncer to >=1.10 (proper SCRAM-SHA-256 support) and confirm auth_type is not a legacy method re-deriving verifiers.
- Verify the host/port in the connection string actually point at a PostgreSQL server, not a proxy.
- If TLS is terminated by a proxy, terminate TLS directly at Postgres so SASL frames pass through unmodified.
Example fix
// before
const client = new Client({ host: 'pgbouncer.internal', port: 6432 })
// after
const client = new Client({ host: 'postgres-primary.internal', port: 5432 }) Defensive patterns
Strategy: try-catch
Try / catch
try {
await client.connect()
} catch (err) {
if (/salt must be base64/.test(err.message)) {
// server SASL frame was malformed -- suspect a proxy/pooler
logger.error('SCRAM frame corruption from intermediary', { err })
return connectBypassingPooler()
}
throw err
} Prevention
- Bypass transaction-poolers when debugging SCRAM auth failures.
- Pin pooler versions that support SCRAM-SHA-256 passthrough (PgBouncer >= 1.10).
- Terminate TLS directly at Postgres rather than at a proxy that may rewrite SASL frames.
When it happens
Trigger: Reached when continueSession (sasl.js:62) is invoked during client.connect() and the parsed `s=` value contains characters outside [A-Za-z0-9+/] or has incorrect `=` padding. The salt is then unusable for the PBKDF2 key derivation at line 116.
Common situations: PgBouncer or another pooler/proxy in transaction-pooling mode that does not relay SCRAM frames correctly; a TLS-terminating reverse proxy that rewrites or truncates the auth exchange; connecting to a non-PostgreSQL server speaking a SASL/SCRAM variant with a different encoding.
Related errors
- SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature must be b
- SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration missing
- SASL: SCRAM-SERVER-FIRST-MESSAGE: invalid iteration count
- SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature is missin
- SASL: Only mechanism(s) ${candidates.join(' and ')} are supp
AI-assisted analysis of brianc/node-postgres@c5e8c9a57b (2026-08-03).
Data as JSON: /data/errors/6858bd8e95450e54.json.
Report an issue: GitHub.