Skip to content

Fix stack buffer overflow on oversized SCRAM salt (CVE-2026-112063) - #272

Open
bmolodan wants to merge 1 commit into
strophe:masterfrom
bmolodan:fix/cve-2026-112063-scram-salt-overflow
Open

bmolodan wants to merge 1 commit into
strophe:masterfrom
bmolodan:fix/cve-2026-112063-scram-salt-overflow

Conversation

@bmolodan

Copy link
Copy Markdown

SCRAM_Hi() concatenates the SCRAM salt with INT(1) into a 128 byte buffer on the
stack, so it can only handle salts of up to 124 bytes. That limit was enforced only by
an assert(), which is compiled out as soon as NDEBUG is defined.

The salt isn't under our control, though. It is taken verbatim from the s= attribute of
the server-first-message, base64-decoded by sasl_scram() without any limit on the
resulting length, and passed down to SCRAM_Hi() through SCRAM_ClientKey():

  • src/sasl.c:427 — xmpp_base64_decode_bin() decodes s= into sval / sval_len,
    unbounded
  • src/sasl.c:457 — sval / sval_len go straight into SCRAM_ClientKey()
  • src/scram.c:181 — assert(salt_len <= sizeof(tmp) - sizeof(int1)), then
    memcpy(tmp, salt, salt_len) at src/scram.c:188

So a malicious or compromised server can send an s= attribute that decodes to more than
124 bytes and overflow the stack frame of SCRAM_Hi(). In a debug build the client
aborts; with NDEBUG the stack is silently corrupted. This is CVE-2026-112063 (affects
0.14.0 and current master).

The fix

  • Turn the assert() into a runtime check and give SCRAM_Hi() an int return value.
  • Propagate the failure through SCRAM_ClientKey() up to sasl_scram(), which already
    aborts the authentication when it can't compute a response — _handle_scram_challenge()
    in src/auth.c:504 takes goto err on a NULL return.
  • Name the limit once as SCRAM_SALT_MAX in src/scram.c and size tmp from it
    (uint8_t tmp[SCRAM_SALT_MAX + sizeof(int1)]), so the buffer and the check can't drift
    apart. tests/test_scram.c picks the constant up through the existing
    #include "scram.c" rather than hardcoding 124.
  • Log the actual cause in sasl_scram() with strophe_error(ctx, "SASL", ...). Without
    it the failure reaches the generic err: label in src/auth.c, which calls
    disconnect_mem_error() and reports "Memory allocation error" — misleading for an
    oversized salt, and it would send someone chasing a memory bug that isn't there.

SCRAM_ClientKey() is declared in src/scram.h, which is not an installed header
(Makefile.am:114 installs only strophe.h), so the signature change is internal.
SCRAM_ClientSignature() and SCRAM_ClientProof() are untouched — both are bounded by
alg->digest_size.

Reproducer

Against unmodified master, NDEBUG build under ASan, driving the real sasl_scram() with
a server-first-message whose s= decodes to 125 bytes:

==38502==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ff7b1f04dc0
WRITE of size 4 at 0x7ff7b1f04dc0 thread T0
    #1 SCRAM_Hi scram.c:189
    #2 SCRAM_ClientKey scram.c:215
    #3 sasl_scram sasl.c:457
SUMMARY: AddressSanitizer: stack-buffer-overflow scram.c:189 in SCRAM_Hi

With the patch, the same harness returns NULL and aborts authentication instead:

salt= 18 bytes -> sasl_scram() = response (accepted)
salt=124 bytes -> sasl_scram() = response (accepted)
salt=125 bytes -> sasl_scram() = NULL (auth aborted)
salt=400 bytes -> sasl_scram() = NULL (auth aborted)

Tests

tests/test_scram.c gains test_salt_len(), run for SCRAM-SHA-1, -SHA-256 and -SHA-512.
It is table-driven like the existing test_df() / test_scram() / test_hmac() and
checks both the at-limit and one-over-limit cases through SCRAM_Hi() and
SCRAM_ClientKey(). It uses the existing ENSURE_EQ from tests/test.h rather than
assert() on purpose: assert() is compiled out under NDEBUG, which is exactly the
configuration this CVE is about, so an assert-based test would pass vacuously there.

Existing RFC 6070, RFC 6120, RFC 2202 and RFC 4231 vectors still pass. Verified in both
debug and -DNDEBUG builds, clean under ASan and -Wall -Wextra -Werror. Removing the
new bounds check makes the test fail loudly (ASan stack-buffer-overflow), so it does pin
the regression.

Note on a deeper fix

Rejecting long salts hardens a self-imposed limit into protocol-visible behaviour — RFC
5802 sets no upper bound on salt length, so a conforming server using an unusually long
salt is now refused. The alternative is to stop concatenating altogether: crypto_HMAC()
already drives the hash through alg->init / alg->update / alg->final, so it could
take the message in two chunks and SCRAM_Hi() could feed it (salt, salt_len) and
(int1, 4) separately. That removes the buffer, the limit and this error path entirely.

I kept this PR to the minimal security fix since it touches a crypto primitive and changes
behaviour for long salts, but I'm happy to follow up with that refactor — or to fold it in
here instead — whichever you prefer.

Unrelated to this CVE but in the same attacker-controlled message: ival at
src/sasl.c:431 is strtol()'d and cast to uint32_t without validation, so i=-1
yields ~4.3 billion HMAC iterations. Happy to send that separately.

SCRAM_Hi() copies the salt into a 128 byte buffer on the stack and
appends INT(1) to it, so it can only handle salts of up to 124 bytes.
That limit was only enforced by an assert(), which is compiled out as
soon as NDEBUG is defined.

The salt isn't under our control though. It is taken verbatim from the
's=' attribute of the server-first-message, base64-decoded by
sasl_scram() without any limit on the resulting length, and passed down
to SCRAM_Hi() through SCRAM_ClientKey(). A malicious server can
therefore send an 's=' attribute that decodes to more than 124 bytes and
overflow the stack frame of SCRAM_Hi(), which aborts the client in debug
builds and silently corrupts the stack otherwise.

Turn the assert() into a runtime check and propagate the failure through
SCRAM_ClientKey() up to sasl_scram(), which already aborts the
authentication when it can't compute a response.

This is CVE-2026-112063.

Reproduced with a 125 byte salt in an NDEBUG build under ASan, before
the fix:

  ERROR: AddressSanitizer: stack-buffer-overflow
  WRITE of size 4 ...
      strophe#1 SCRAM_Hi scram.c:189
      strophe#2 SCRAM_ClientKey scram.c:215
      strophe#3 sasl_scram sasl.c:457

Signed-off-by: Bohdan <bohdan.molodan@gmail.com>

@Pandorus2012 Pandorus2012 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

looks good

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants