jackc/pgx · critical

invalid SCRAM nonce: did not start with client nonce

Error message

invalid SCRAM nonce: did not start with client nonce

What it means

Returned by recvServerFirstMessage when the combined nonce (r=) from the server does not start with the client's own nonce. Per RFC 5802 the server must concatenate the client nonce with its own nonce, so a prefix mismatch signals a broken or malicious server that did not echo the client nonce.

Source

Thrown at pgconn/auth_scram.go:296

	sc.salt, err = base64.StdEncoding.DecodeString(string(saltStr))
	if err != nil {
		return fmt.Errorf("invalid SCRAM salt received from server: %w", err)
	}

	sc.iterations, err = strconv.Atoi(string(iterationsStr))
	if err != nil || sc.iterations <= 0 {
		return fmt.Errorf("invalid SCRAM iteration count received from server: %w", err)
	}
	// Bound server-supplied iteration count to prevent a malicious server from forcing the client
	// to spend unbounded CPU in PBKDF2. PostgreSQL's scram_iterations defaults to 4096; this ceiling
	// is ~2500x that.
	const maxScramIterations = 10_000_000
	if sc.iterations > maxScramIterations {
		return fmt.Errorf("SCRAM iteration count from server too high: %d (max %d)", sc.iterations, maxScramIterations)
	}

	if !bytes.HasPrefix(sc.clientAndServerNonce, sc.clientNonce) {
		return errors.New("invalid SCRAM nonce: did not start with client nonce")
	}

	if len(sc.clientAndServerNonce) <= len(sc.clientNonce) {
		return errors.New("invalid SCRAM nonce: did not include server nonce")
	}

	return nil
}

func (sc *scramClient) clientFinalMessage() string {
	// The c= attribute carries the base64-encoded channel binding input.
	//
	// Without channel binding this is just the GS2 header alone ("biws" for
	// "n,," or "eSws" for "y,,").
	//
	// With channel binding, this is the GS2 header with the channel binding data
	// (certificate hash) appended.
	channelBindInput := sc.clientGS2Header

View on GitHub (pinned to ec1a0befd2)

Solutions

  1. Treat this as a potential security incident: verify the server identity and network path (use TLS + verify-full).
  2. Ensure no intermediary is terminating/reoriginating the SASL handshake.
  3. Confirm the server is genuine PostgreSQL; capture the handshake if it recurs.
Defensive patterns

Strategy: try-catch

Try / catch

// A nonce prefix mismatch is a strong MITM signal; do NOT silently retry.
if err := connect(); err != nil && strings.Contains(err.Error(), "did not start with client nonce") {
    securityLog.Error("possible SCRAM MITM detected: server nonce prefix mismatch", "err", err)
    return errors.Join(err, errPossibleMITM)
}

Prevention

When it happens

Trigger: Server returns an r= value whose prefix differs from the clientNonce generated by newScramClient. Classic indicator of a rogue server/MITM that fabricated its own nonce instead of extending the client's.

Common situations: Man-in-the-middle attempting a SCRAM downgrade/relay; buggy server generating a fresh nonce; protocol corruption.

Related errors


AI-assisted analysis of jackc/pgx@ec1a0befd2 (2026-08-04). Data as JSON: /data/errors/8b831446e3aa3411.json. Report an issue: GitHub.