GHSA-F5V4-2WR6-HQMG

Vulnerability from github – Published: 2026-04-24 15:39 – Updated: 2026-05-13 13:34
VLAI
Summary
russh has pre-auth DoS via unbounded allocation in its keyboard-interactive auth handler
Details

Summary

A pre-authentication denial-of-service vulnerability exists in the server's keyboard-interactive authentication handler. A malicious client can crash any russh-based server that implements keyboard-interactive auth (e.g., for 2FA/TOTP) with a single malformed packet, requiring no credentials.

Vulnerability Details

In russh/src/server/encrypted.rs, the function read_userauth_info_response decodes a u32 count from the client's SSH_MSG_USERAUTH_INFO_RESPONSE and passes it directly to Vec::with_capacity():

let n = map_err!(u32::decode(r))?;

// Bound both allocation and iteration by remaining packet data to
// prevent a malicious client from causing a multi-GB allocation or
// billions of loop iterations with a crafted count.
// Each response needs at least 4 bytes (length prefix).
let max_responses = r.remaining_len().saturating_add(3) / 4;
let n = (n as usize).min(max_responses);
let mut responses = Vec::with_capacity(n);
for _ in 0..n {
    responses.push(Bytes::decode(r).ok())
}

An attacker can send n = 0x10000000 (268M) or larger in a minimal packet (~50 bytes after encryption). The server attempts to allocate n * ~24 bytes (size of Option<Bytes>) = ~6.4GB, causing an OOM crash.

Attack Flow

  1. Attacker connects via TCP, completes key exchange (no credentials needed -- this is the anonymous DH handshake, not authentication)
  2. Sends USERAUTH_REQUEST with method keyboard-interactive
  3. Server handler returns Auth::Partial with prompts (standard for 2FA/TOTP)
  4. Attacker sends USERAUTH_INFO_RESPONSE with n = 0x10000000 and no response data
  5. Server calls Vec::with_capacity(268_435_456), OOM killed

No authentication is required. The allocation occurs before the handler validates any credentials. The attack is repeatable faster than the server can restart.

Affected Configurations

Any russh-based server where the Handler::auth_keyboard_interactive implementation returns Auth::Partial (i.e., sends prompts to the client). The default handler returns Auth::reject() and is not affected.

Source code review suggests that downstream projects using keyboard-interactive for multi-step auth (e.g., TOTP/2FA) follow the affected pattern, since returning Auth::Partial before credential verification is the intended API usage for prompting.

Confirmed End-to-End PoC

There is a complete Docker-contained PoC confirming the OOM kill: - Minimal russh server returning Auth::Partial for keyboard-interactive - Python client (paramiko for key exchange) sends malformed USERAUTH_INFO_RESPONSE - Container with 512MB memory limit; server is OOM-killed (exit code 137)

Available on request.

Proposed Fix

Cap the Vec::with_capacity allocation to what the remaining packet data can actually contain. Each response requires at least 4 bytes (length prefix), so:

let n = map_err!(u32::decode(r))?;

// Bound both allocation and iteration by remaining packet data to
// prevent a malicious client from causing a multi-GB allocation or
// billions of loop iterations with a crafted count.
// Each response needs at least 4 bytes (length prefix).
let max_responses = r.remaining_len().saturating_add(3) / 4;
let n = (n as usize).min(max_responses);
let mut responses = Vec::with_capacity(n);
for _ in 0..n {
    responses.push(Bytes::decode(r).ok())
}

This bounds the allocation to at most the packet size (~256KB), while preserving the existing behavior for well-formed packets. This fix has been implemented, tested, and contributed via the temporary private fork.

Severity

Pre-auth, remote, no credentials required, crashes the server process affecting all active sessions.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "crates.io",
        "name": "russh"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.60.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-42189"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-770",
      "CWE-789"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-24T15:39:37Z",
    "nvd_published_at": "2026-05-08T20:16:31Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nA pre-authentication denial-of-service vulnerability exists in the server\u0027s keyboard-interactive authentication handler. A malicious client can crash any russh-based server that implements keyboard-interactive auth (e.g., for 2FA/TOTP) with a single malformed packet, requiring no credentials.\n\n## Vulnerability Details\n\nIn `russh/src/server/encrypted.rs`, the function `read_userauth_info_response` decodes a `u32` count from the client\u0027s `SSH_MSG_USERAUTH_INFO_RESPONSE` and passes it directly to `Vec::with_capacity()`:\n\n```rust\nlet n = map_err!(u32::decode(r))?;\n\n// Bound both allocation and iteration by remaining packet data to\n// prevent a malicious client from causing a multi-GB allocation or\n// billions of loop iterations with a crafted count.\n// Each response needs at least 4 bytes (length prefix).\nlet max_responses = r.remaining_len().saturating_add(3) / 4;\nlet n = (n as usize).min(max_responses);\nlet mut responses = Vec::with_capacity(n);\nfor _ in 0..n {\n    responses.push(Bytes::decode(r).ok())\n}\n```\n\nAn attacker can send `n = 0x10000000` (268M) or larger in a minimal packet (~50 bytes after encryption). The server attempts to allocate `n * ~24 bytes` (size of `Option\u003cBytes\u003e`) = ~6.4GB, causing an OOM crash.\n\n## Attack Flow\n\n1. Attacker connects via TCP, completes key exchange (no credentials needed -- this is the anonymous DH handshake, not authentication)\n2. Sends `USERAUTH_REQUEST` with method `keyboard-interactive`\n3. Server handler returns `Auth::Partial` with prompts (standard for 2FA/TOTP)\n4. Attacker sends `USERAUTH_INFO_RESPONSE` with `n = 0x10000000` and no response data\n5. Server calls `Vec::with_capacity(268_435_456)`, OOM killed\n\nNo authentication is required. The allocation occurs before the handler validates any credentials. The attack is repeatable faster than the server can restart.\n\n## Affected Configurations\n\nAny russh-based server where the `Handler::auth_keyboard_interactive` implementation returns `Auth::Partial` (i.e., sends prompts to the client). The default handler returns `Auth::reject()` and is not affected.\n\nSource code review suggests that downstream projects using keyboard-interactive for multi-step auth (e.g., TOTP/2FA) follow the affected pattern, since returning `Auth::Partial` before credential verification is the intended API usage for prompting.\n\n## Confirmed End-to-End PoC\n\nThere is a complete Docker-contained PoC confirming the OOM kill:\n- Minimal russh server returning `Auth::Partial` for keyboard-interactive\n- Python client (paramiko for key exchange) sends malformed `USERAUTH_INFO_RESPONSE`\n- Container with 512MB memory limit; server is OOM-killed (exit code 137)\n\nAvailable on request.\n\n## Proposed Fix\n\nCap the `Vec::with_capacity` allocation to what the remaining packet data can actually contain. Each response requires at least 4 bytes (length prefix), so:\n\n```rust\nlet n = map_err!(u32::decode(r))?;\n\n// Bound both allocation and iteration by remaining packet data to\n// prevent a malicious client from causing a multi-GB allocation or\n// billions of loop iterations with a crafted count.\n// Each response needs at least 4 bytes (length prefix).\nlet max_responses = r.remaining_len().saturating_add(3) / 4;\nlet n = (n as usize).min(max_responses);\nlet mut responses = Vec::with_capacity(n);\nfor _ in 0..n {\n    responses.push(Bytes::decode(r).ok())\n}\n```\n\nThis bounds the allocation to at most the packet size (~256KB), while preserving the existing behavior for well-formed packets. This fix has been implemented, tested, and contributed via the temporary private fork.\n\n## Severity\n\nPre-auth, remote, no credentials required, crashes the server process affecting all active sessions.",
  "id": "GHSA-f5v4-2wr6-hqmg",
  "modified": "2026-05-13T13:34:53Z",
  "published": "2026-04-24T15:39:37Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Eugeny/russh/security/advisories/GHSA-f5v4-2wr6-hqmg"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42189"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Eugeny/russh/commit/6c3c80a9b6d60763d6227d60fa8310e57172a4d1"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Eugeny/russh"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Eugeny/russh/releases/tag/v0.60.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "russh has pre-auth DoS via unbounded allocation in its keyboard-interactive auth handler"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.