Skip to content

[Security] SCRAM-SHA-256 accepts dangerously low iteration count from server #3655

Description

@eddieran

Split from #3651 as suggested by @charmander.

Summary

The SCRAM-SHA-256 implementation accepts any iteration count >= 1 from the server. A rogue PostgreSQL server can send a SCRAM challenge with i=1, making the derived key trivially cheap to brute-force offline.

Affected code

packages/pg/lib/crypto/sasl.js — parseServerFirstMessage():

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)

The regex only validates that the iteration count is a positive integer. There is no minimum bound check.

Threat model

This is a rogue server scenario. If a client connects to a malicious PostgreSQL server, the server controls the SCRAM ServerFirstMessage including the iteration count. By setting i=1, the server receives the client's SCRAM proof computed with only 1 PBKDF2 iteration. An attacker who captures this exchange can brute-force the password offline at effectively plaintext speed.

RFC 7677 (SCRAM-SHA-256) Section 4 states:

the minimum iteration count SHOULD be at least 4096

Steps to reproduce

  1. Set up a mock PostgreSQL server that responds to SCRAM-SHA-256 with r=<nonce>,s=<salt>,i=1
  2. Connect a pg client with a password
  3. The client completes the SCRAM exchange using only 1 PBKDF2 iteration
  4. The attacker now has a proof derived with 1 iteration — offline brute-force is trivial

Impact

Password compromise via offline brute-force after a single observed authentication exchange with a rogue server.

Suggested fix

Enforce a minimum iteration count in parseServerFirstMessage():

const iteration = parseInt(iterationText, 10)
if (iteration < 4096) {
  throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration count ' + iteration + ' is below minimum 4096')
}

Activity

  1. sehrope commented on Apr 13, 2026

    @sehrope
    Contributor

    This is somewhat intentional as there is valid use cases for an iteration count of 1. Namely if your password is already long and cryptographically random (as it should be, e.g., tr -d -c 'a-zA-Z0-9' </dev/urandom | head -c 64), then there's nothing that's actually possible to brute force even with a single iteration. For applications that create a lot of connections (e.g., talking to an intermediate pooler that holds the "real" connections) the overhead of a high iteration count is a measurable hit.

    If you're worried about a malicious server, a more meaningful patch may be to limit the auth mechanisms and iteration counts. For pgjdbc (the Java driver) we have connection properties that allow the user to mandate that SCRAM is used. Adding something like that to pg would be useful to enforce the auth mechanism. It's orthogonal to things like sslmode to require TLS or verify the cert.

    The protocol flow for PG has the server decide the auth mechanism and it asks the client for it. A malicious server can even ask for the plaintext password (see AuthenticationCleartextPassword in https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-START-UP) and a compliant client will send it back. That exists as an escape hatch to allow for arbitrary authentication schemes as it's passed as-is to the auth handler (e.g., AWS RDS uses this for IAM based to have the client send a signature that they can read on the server side, though they also mandate TLS).

    We might have a more real problem with a malicious server that sends a huge iteration count, e.g., i=9007199254740991. On nodejs that would lock up the single thread CPU effectively forever trying to calculate the PBKDF2.

  2. charmander commented on Apr 14, 2026

    @charmander
    Collaborator

    If you're worried about a malicious server, a more meaningful patch may be to limit the auth mechanisms and iteration counts. For pgjdbc (the Java driver) we have connection properties that allow the user to mandate that SCRAM is used. Adding something like that to pg would be useful to enforce the auth mechanism.

    Yes, I think that’s worth adding (support for require_auth like libpq). Iteration counts are interesting: anyone who knows to configure a lower bound probably also knows to use a secure password that makes iteration moot. Is supporting bad passwords important enough to break backward compatibility? As for an upper bound, pretending to be resistant to DoS from a malicious server might be a false sense of security (I’m sure the rest of pg isn’t), but maybe we should implement one anyway.

  3. eddieran commented on Apr 16, 2026

    @eddieran
    Author

    Thanks @sehrope and @charmander for the thorough analysis — I agree with all of it.

    Re-reading with your points, the threat model is narrower than I initially framed:

    1. Low iteration count (i=1): agreed this isn't a meaningful risk given cryptographically random passwords are the norm, and adding a lower bound is a false sense of security for users with weak passwords. Not worth breaking backward compatibility for.
    2. Huge iteration count (e.g. i=9007199254740991): this is the real concern — a malicious server can CPU-lock the Node event loop on PBKDF2. This is a legitimate server-in-the-middle DoS and has nothing to do with password strength. An upper bound (something like i <= 1_000_000, matching libpq) would prevent it.
    3. require_auth like libpq: separate-but-related feature. Happy to track this separately.

    Would a PR that just caps the server-advertised iteration count (rejecting i > 1_000_000 with a clear error) be the right scope here? The require_auth mechanism controls are orthogonal and probably deserve their own issue.

  4. brianc commented on May 11, 2026

    @brianc
    Owner

    Would a PR that just caps the server-advertised iteration count (rejecting i > 1_000_000 with a clear error) be the right scope here? The require_auth mechanism controls are orthogonal and probably deserve their own issue.

    Yea I think that makes sense. Not a big concern, but might as well not accept wildly inappropriate iteration counts.

  5. eddieran commented on May 16, 2026

    @eddieran
    Author

    Thanks @brianc. I'll send a PR that adds an iteration-count ceiling in packages/pg/lib/crypto/sasl.js — rejecting any i > 1_000_000 from the server with a clear error message that names SCRAM and the iteration field. Will hold off on the require_auth/mechanism-allowlist work for a separate issue per @charmander's suggestion above.

  6. sehrope commented on May 16, 2026

    @sehrope
    Contributor

    @eddieran See #3677 for a WIP PR that adds the max scram iterations (capped at 100K not 1M). There's some testing related feedback that needs to be addressed and planning on it next week. But feel free to pick it up before then.

  7. eddieran commented on May 24, 2026

    @eddieran
    Author

    Saw #3677 landed 2026-05-18 with a 100K cap — that handles the unbounded-iteration angle. The cap value is also more conservative than what I'd have proposed (common production sha-256 sources sit at 4096-15000, so 100K headroom is generous).

    Closing on my end. Happy to reopen if anything related surfaces later.

  8. brianc commented on May 24, 2026

    @brianc
    Owner
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions