VYPR
Medium severity6.2NVD Advisory· Published Jan 30, 2026· Updated Apr 15, 2026

CVE-2025-62349

CVE-2025-62349

Description

Salt contains an authentication protocol version downgrade weakness that can allow a malicious minion to bypass newer authentication/security features by using an older request payload format, enabling minion impersonation and circumventing protections introduced in response to prior issues.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
saltPyPI
>= 3006.12, < 3006.173006.17
saltPyPI
>= 3007.4, < 3007.93007.9

Affected products

1

Patches

1
3d5708acae16

Add minimum_auth_version to enforce all security

https://github.com/saltstack/saltDaniel A. WozniakNov 19, 2025via ghsa
6 files changed · +658 1
  • changelog/68467.fixed.md+1 0 added
    @@ -0,0 +1 @@
    +Fixed authentication protocol version downgrade vulnerability (CVE-2025-62349) by adding `minimum_auth_version` configuration option to prevent minions from bypassing security features through protocol downgrade attacks.
    
  • doc/ref/configuration/master.rst+57 0 modified
    @@ -2062,6 +2062,63 @@ master's request channel. Valid values are ``PKCS1v15-SHA1`` and
     ``PKCS1v15-SHA224``. Minions must be at version ``3006.9`` or greater if this
     is changed from the default setting.
     
    +.. conf_master:: minimum_auth_version
    +
    +``minimum_auth_version``
    +------------------------
    +
    +.. versionadded:: 3006.17,3007.9
    +
    +Default: ``0``
    +
    +Enforce a minimum authentication protocol version from minions connecting to the master.
    +This setting protects against authentication downgrade attacks (CVE-2025-62349) where a
    +malicious minion attempts to use an older, less secure authentication protocol version to
    +bypass security features introduced in newer protocol versions.
    +
    +Authentication protocol versions and their security features:
    +
    +- **Version 0/1**: No message signing, no nonce, no security features (legacy, insecure)
    +- **Version 2**: Message signing and nonce, but missing TTL validation, token validation,
    +  and minion ID matching (partially secure)
    +- **Version 3+**: Full security with message signing, nonce, TTL checks, token validation,
    +  minion ID matching, and session keys (recommended)
    +
    +**Important Security Considerations:**
    +
    +The default value of ``0`` allows all authentication protocol versions for backward
    +compatibility during rolling upgrades (where the master is typically upgraded before minions).
    +
    +**Recommended value:** ``3`` - Once all minions in your infrastructure have been upgraded
    +to a version that supports protocol version 3 or higher, set this value to ``3`` to ensure
    +maximum security.
    +
    +**Upgrade Path:**
    +
    +1. Upgrade your Salt Master to a version supporting ``minimum_auth_version``
    +2. Keep the default value of ``0`` during the minion upgrade process
    +3. Upgrade all minions to a version supporting authentication protocol v3+
    +4. Set ``minimum_auth_version: 3`` in the master configuration
    +5. Restart the Salt Master to enforce the new security requirement
    +
    +.. code-block:: yaml
    +
    +    # Default - allows all versions (backward compatible but less secure)
    +    minimum_auth_version: 0
    +
    +    # Recommended - enforces modern authentication protocol (secure)
    +    minimum_auth_version: 3
    +
    +.. warning::
    +    Setting ``minimum_auth_version`` to a value higher than what your minions support
    +    will prevent those minions from authenticating. Ensure all minions are upgraded
    +    before increasing this value. Check your minion versions before changing this setting.
    +
    +.. note::
    +    When a minion's authentication is rejected due to insufficient protocol version,
    +    a warning message will be logged on the master including the minion ID and the
    +    protocol version it attempted to use.
    +
     
     ``ssl``
     -------
    
  • salt/channel/server.py+16 1 modified
    @@ -136,7 +136,22 @@ def handle_message(self, payload):
             ):
                 log.warn("bad load received on socket")
                 raise salt.ext.tornado.gen.Return("bad load")
    -        version = payload.get("version", 0)
    +        try:
    +            version = int(payload.get("version", 0))
    +        except ValueError:
    +            version = 0
    +
    +        # Enforce minimum authentication protocol version to prevent downgrade attacks
    +        minimum_version = self.opts.get("minimum_auth_version", 0)
    +        if minimum_version > 0 and version < minimum_version:
    +            log.warning(
    +                "Rejected authentication attempt using protocol version %d "
    +                "(minimum required: %d)",
    +                version,
    +                minimum_version,
    +            )
    +            raise salt.ext.tornado.gen.Return("bad load")
    +
             try:
                 payload = self._decode_payload(payload, version)
             except Exception as exc:  # pylint: disable=broad-except
    
  • salt/config/__init__.py+3 0 modified
    @@ -1006,6 +1006,8 @@ def _gather_buffer_space():
             "publish_signing_algorithm": str,
             "request_server_ttl": int,
             "request_server_aes_session": int,
    +        # Minimum authentication protocol version to accept from minions
    +        "minimum_auth_version": int,
         }
     )
     
    @@ -1666,6 +1668,7 @@ def _gather_buffer_space():
             "publish_signing_algorithm": "PKCS1v15-SHA1",
             "request_server_aes_session": 0,
             "request_server_ttl": 0,
    +        "minimum_auth_version": 0,
         }
     )
     
    
  • tests/pytests/functional/channel/test_auth_downgrade.py+319 0 added
    @@ -0,0 +1,319 @@
    +"""
    +Functional tests for authentication version downgrade attacks.
    +
    +These tests demonstrate the vulnerability where a malicious minion can
    +downgrade its authentication protocol version to bypass security features.
    +"""
    +
    +import ctypes
    +import multiprocessing
    +import pathlib
    +
    +import pytest
    +
    +import salt.channel.client
    +import salt.channel.server
    +import salt.config
    +import salt.crypt
    +import salt.daemons.masterapi
    +import salt.ext.tornado.gen
    +import salt.ext.tornado.ioloop
    +import salt.master
    +import salt.payload
    +import salt.utils.event
    +import salt.utils.files
    +import salt.utils.platform
    +import salt.utils.process
    +import salt.utils.stringutils
    +from salt.master import SMaster
    +
    +pytestmark = [
    +    pytest.mark.skip_on_spawning_platform(
    +        reason="These tests are currently broken on spawning platforms. Need to be rewritten.",
    +    ),
    +    pytest.mark.timeout_unless_on_windows(120),
    +]
    +
    +
    +@pytest.fixture
    +def channel_minion_id():
    +    return "test-minion-downgrade"
    +
    +
    +@pytest.fixture
    +def auth_pki_dir(tmp_path):
    +    """Setup PKI directory structure for functional auth tests."""
    +    if salt.utils.platform.is_darwin():
    +        # To avoid 'OSError: AF_UNIX path too long'
    +        _root_dir = pathlib.Path("/tmp").resolve() / tmp_path.name
    +        root_dir = _root_dir
    +    else:
    +        root_dir = tmp_path
    +
    +    master_pki = root_dir / "master_pki"
    +    minion_pki = root_dir / "minion_pki"
    +
    +    master_pki.mkdir(parents=True)
    +    minion_pki.mkdir(parents=True)
    +    (master_pki / "minions").mkdir()
    +    (master_pki / "minions_pre").mkdir()
    +    (master_pki / "minions_rejected").mkdir()
    +    (master_pki / "minions_denied").mkdir()
    +
    +    # Generate keys
    +    salt.crypt.gen_keys(str(master_pki), "master", 4096)
    +    salt.crypt.gen_keys(str(minion_pki), "minion", 4096)
    +
    +    yield root_dir
    +
    +    if salt.utils.platform.is_darwin():
    +        import shutil
    +
    +        shutil.rmtree(str(root_dir), ignore_errors=True)
    +
    +
    +def _create_functional_master_opts(
    +    auth_pki_dir, channel_minion_id, minimum_auth_version=0
    +):
    +    """Helper to create master configuration with specified minimum_auth_version."""
    +    opts = {
    +        "master_uri": "tcp://127.0.0.1:44506",
    +        "interface": "127.0.0.1",
    +        "ret_port": 44506,
    +        "ipv6": False,
    +        "sock_dir": str(auth_pki_dir / "sock"),
    +        "cachedir": str(auth_pki_dir / "cache"),
    +        "pki_dir": str(auth_pki_dir / "master_pki"),
    +        "id": "master",
    +        "__role": "master",
    +        "transport": "zeromq",
    +        "keysize": 4096,
    +        "max_minions": 0,
    +        "auto_accept": True,
    +        "open_mode": False,
    +        "key_pass": None,
    +        "publish_port": 44505,
    +        "auth_mode": 1,
    +        "auth_events": True,
    +        "publish_session": 86400,
    +        "request_server_ttl": 300,  # 5 minutes
    +        "worker_threads": 1,
    +        "cluster_id": None,
    +        "master_sign_pubkey": False,
    +        "sign_pub_messages": False,
    +        "minimum_auth_version": minimum_auth_version,
    +    }
    +    (auth_pki_dir / "sock").mkdir(exist_ok=True)
    +    (auth_pki_dir / "cache").mkdir(exist_ok=True)
    +    (auth_pki_dir / "cache" / "sessions").mkdir(exist_ok=True)
    +
    +    # Accept the minion key
    +    minion_pub = auth_pki_dir / "minion_pki" / "minion.pub"
    +    master_minions = auth_pki_dir / "master_pki" / "minions" / channel_minion_id
    +    if minion_pub.exists():
    +        with salt.utils.files.fopen(str(minion_pub)) as src:
    +            with salt.utils.files.fopen(str(master_minions), "w") as dst:
    +                dst.write(src.read())
    +
    +    return opts
    +
    +
    +@pytest.fixture
    +def functional_master_opts(auth_pki_dir, channel_minion_id):
    +    """Master configuration for functional tests (default: minimum_auth_version=3)."""
    +    return _create_functional_master_opts(
    +        auth_pki_dir, channel_minion_id, minimum_auth_version=3
    +    )
    +
    +
    +@pytest.fixture
    +def functional_minion_opts(auth_pki_dir, functional_master_opts, channel_minion_id):
    +    """Minion configuration for functional tests."""
    +    return {
    +        "master_uri": "tcp://127.0.0.1:44506",
    +        "interface": "127.0.0.1",
    +        "ret_port": 44506,
    +        "ipv6": False,
    +        "sock_dir": str(functional_master_opts["sock_dir"]),
    +        "pki_dir": str(auth_pki_dir / "minion_pki"),
    +        "id": channel_minion_id,
    +        "__role": "minion",
    +        "transport": "zeromq",
    +        "keysize": 4096,
    +        "encryption_algorithm": salt.crypt.OAEP_SHA1,
    +        "signing_algorithm": salt.crypt.PKCS1v15_SHA1,
    +        "master_port": 44506,
    +        "master_ip": "127.0.0.1",
    +    }
    +
    +
    +@pytest.mark.parametrize(
    +    "minimum_auth_version,attack_version,should_pass",
    +    [
    +        pytest.param(
    +            0,
    +            0,
    +            False,
    +            marks=pytest.mark.xfail(
    +                reason="Vulnerable: minimum_auth_version=0 allows all versions",
    +                strict=True,
    +            ),
    +        ),
    +        pytest.param(
    +            0,
    +            1,
    +            False,
    +            marks=pytest.mark.xfail(
    +                reason="Vulnerable: minimum_auth_version=0 allows all versions",
    +                strict=True,
    +            ),
    +        ),
    +        pytest.param(
    +            0,
    +            2,
    +            False,
    +            marks=pytest.mark.xfail(
    +                reason="Vulnerable: minimum_auth_version=0 allows all versions",
    +                strict=True,
    +            ),
    +        ),
    +        pytest.param(1, 0, True, id="v1-rejects-v0"),
    +        pytest.param(
    +            1,
    +            1,
    +            False,
    +            marks=pytest.mark.xfail(
    +                reason="Vulnerable: minimum_auth_version=1 allows v1", strict=True
    +            ),
    +        ),
    +        pytest.param(
    +            1,
    +            2,
    +            False,
    +            marks=pytest.mark.xfail(
    +                reason="Vulnerable: minimum_auth_version=1 allows v2", strict=True
    +            ),
    +        ),
    +        pytest.param(2, 0, True, id="v2-rejects-v0"),
    +        pytest.param(2, 1, True, id="v2-rejects-v1"),
    +        pytest.param(
    +            2,
    +            2,
    +            False,
    +            marks=pytest.mark.xfail(
    +                reason="Vulnerable: minimum_auth_version=2 allows v2", strict=True
    +            ),
    +        ),
    +        pytest.param(3, 0, True, id="v3-rejects-v0"),
    +        pytest.param(3, 1, True, id="v3-rejects-v1"),
    +        pytest.param(3, 2, True, id="v3-rejects-v2-SECURE"),
    +    ],
    +)
    +async def test_replay_attack_via_version_downgrade(
    +    auth_pki_dir,
    +    functional_minion_opts,
    +    channel_minion_id,
    +    minimum_auth_version,
    +    attack_version,
    +    should_pass,
    +):
    +    """
    +    REGRESSION TEST: CVE-TBD - Replay Attack via Version Downgrade
    +
    +    This parameterized test verifies that version downgrade attacks are prevented
    +    based on the minimum_auth_version configuration.
    +
    +    Attack Scenario:
    +    1. A legitimate minion sends a version 3 authenticated message
    +    2. An attacker captures the encrypted payload
    +    3. The attacker attempts to replay it with an older version to bypass security
    +
    +    Test Matrix:
    +    - minimum_auth_version=0: VULNERABLE - accepts all versions (xfail)
    +    - minimum_auth_version=1: Partially secure - rejects v0 only
    +    - minimum_auth_version=2: Partially secure - rejects v0, v1
    +    - minimum_auth_version=3: SECURE - rejects v0, v1, v2 (recommended)
    +
    +    This test uses xfail for vulnerable configurations to document the security risk.
    +    """
    +    # Create master opts with the specified minimum_auth_version
    +    master_opts = _create_functional_master_opts(
    +        auth_pki_dir, channel_minion_id, minimum_auth_version
    +    )
    +
    +    # Setup master secrets
    +    SMaster.secrets["aes"] = {
    +        "secret": multiprocessing.Array(
    +            ctypes.c_char,
    +            salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()),
    +        ),
    +        "serial": multiprocessing.Value(ctypes.c_longlong, lock=False),
    +    }
    +
    +    # Create server channel
    +    server_channel = salt.channel.server.ReqServerChannel.factory(master_opts)
    +    server_channel.auto_key = salt.daemons.masterapi.AutoKey(master_opts)
    +    server_channel.cache_cli = False
    +    server_channel.event = salt.utils.event.get_master_event(
    +        master_opts, master_opts["sock_dir"], listen=False
    +    )
    +    server_channel.master_key = salt.crypt.MasterKeys(master_opts)
    +
    +    try:
    +        # Verify the configuration
    +        assert (
    +            master_opts.get("minimum_auth_version") == minimum_auth_version
    +        ), f"Master should have minimum_auth_version={minimum_auth_version}"
    +
    +        # Read minion public key
    +        with salt.utils.files.fopen(
    +            str(auth_pki_dir / "minion_pki" / "minion.pub"), "r"
    +        ) as fp:
    +            pub_key = fp.read()
    +
    +        # Create auth load for the attack version
    +        load = {
    +            "cmd": "_auth",
    +            "id": channel_minion_id,
    +            "pub": pub_key,
    +            "enc_algo": salt.crypt.OAEP_SHA1,
    +            "sig_algo": salt.crypt.PKCS1v15_SHA1,
    +        }
    +
    +        # Add nonce for version 2+
    +        if attack_version >= 2:
    +            load["nonce"] = "test-nonce"
    +
    +        # Create payload for handle_message
    +        payload = {"enc": "clear", "load": load, "version": attack_version}
    +
    +        # Call handle_message which will enforce minimum_auth_version
    +        ret = await server_channel.handle_message(payload)
    +
    +        # Check if attack was blocked
    +        if should_pass:
    +            # Attack should be blocked (old version rejected)
    +            # With proper minimum_auth_version, handle_message returns "bad load"
    +            assert ret == "bad load", (
    +                f"Expected 'bad load' for rejected version {attack_version} "
    +                f"with minimum {minimum_auth_version}"
    +            )
    +        else:
    +            # Vulnerable: attack succeeds (old version accepted)
    +            # Assert that auth SHOULD be rejected (but it won't be - causing xfail)
    +            # This assertion will FAIL for vulnerable configs, documenting the security issue
    +            assert ret == "bad load", (
    +                f"SECURITY ISSUE: Version {attack_version} was accepted "
    +                f"with minimum_auth_version={minimum_auth_version}"
    +            )
    +
    +    finally:
    +        server_channel.close()
    +        if "aes" in SMaster.secrets:
    +            SMaster.secrets.pop("aes")
    +
    +
    +# Note: Additional functional tests for ID mismatch and token bypass
    +# have been covered in the unit tests. This single functional test
    +# demonstrates the replay attack scenario which is the most critical
    +# end-to-end attack vector.
    
  • tests/pytests/unit/channel/test_server.py+262 0 modified
    @@ -1,6 +1,18 @@
    +import ctypes
    +import multiprocessing
    +import uuid
    +
     import pytest
     
     import salt.channel.server as server
    +import salt.crypt
    +import salt.daemons.masterapi
    +import salt.master
    +import salt.payload
    +import salt.utils.event
    +import salt.utils.files
    +import salt.utils.stringutils
    +from salt.master import SMaster
     
     
     @pytest.fixture
    @@ -86,3 +98,253 @@ def test_req_server_validate_token_removes_token_id_traversal(root_dir):
         }
         assert reqsrv.validate_token(payload) is False
         assert "tok" not in payload["load"]
    +
    +
    +# ============================================================================
    +# Auth Version Downgrade Attack Regression Tests
    +# ============================================================================
    +
    +
    +@pytest.fixture
    +def pki_dir(tmp_path):
    +    """Setup PKI directory structure for auth tests."""
    +    master_pki = tmp_path / "master"
    +    minion_pki = tmp_path / "minion"
    +
    +    master_pki.mkdir()
    +    minion_pki.mkdir()
    +    (master_pki / "minions").mkdir()
    +    (master_pki / "minions_pre").mkdir()
    +    (master_pki / "minions_rejected").mkdir()
    +    (master_pki / "minions_denied").mkdir()
    +
    +    # Generate master keys
    +    salt.crypt.gen_keys(str(master_pki), "master", 4096)
    +
    +    # Generate minion keys
    +    salt.crypt.gen_keys(str(minion_pki), "minion", 4096)
    +
    +    return tmp_path
    +
    +
    +@pytest.fixture
    +def auth_master_opts(pki_dir, tmp_path):
    +    """Master configuration for auth tests."""
    +    opts = {
    +        "master_uri": "tcp://127.0.0.1:4506",
    +        "interface": "127.0.0.1",
    +        "ret_port": 4506,
    +        "ipv6": False,
    +        "sock_dir": str(tmp_path / "sock"),
    +        "cachedir": str(tmp_path / "cache"),
    +        "pki_dir": str(pki_dir / "master"),
    +        "id": "master",
    +        "__role": "master",
    +        "keysize": 4096,
    +        "max_minions": 0,
    +        "auto_accept": False,
    +        "open_mode": False,
    +        "key_pass": None,
    +        "publish_port": 4505,
    +        "auth_mode": 1,
    +        "auth_events": True,
    +        "publish_session": 86400,
    +        "request_server_ttl": 300,  # 5 minutes
    +        "master_sign_pubkey": False,
    +        "sign_pub_messages": False,
    +        "cluster_id": None,
    +        "transport": "zeromq",
    +        "minimum_auth_version": 3,  # Enforce version 3+ for security
    +    }
    +    (tmp_path / "sock").mkdir(exist_ok=True)
    +    (tmp_path / "cache").mkdir(exist_ok=True)
    +    (tmp_path / "cache" / "sessions").mkdir(exist_ok=True)
    +    return opts
    +
    +
    +@pytest.fixture
    +def auth_minion_opts(pki_dir, auth_master_opts):
    +    """Minion configuration for auth tests."""
    +    return {
    +        "master_uri": "tcp://127.0.0.1:4506",
    +        "interface": "127.0.0.1",
    +        "ret_port": 4506,
    +        "ipv6": False,
    +        "sock_dir": str(auth_master_opts["sock_dir"]),
    +        "pki_dir": str(pki_dir / "minion"),
    +        "id": "test-minion",
    +        "__role": "minion",
    +        "keysize": 4096,
    +        "encryption_algorithm": salt.crypt.OAEP_SHA1,
    +        "signing_algorithm": salt.crypt.PKCS1v15_SHA1,
    +    }
    +
    +
    +@pytest.fixture
    +def setup_accepted_minion(pki_dir, auth_minion_opts, auth_master_opts):
    +    """
    +    Setup a pre-accepted minion by copying its public key to master's
    +    minions directory.
    +    """
    +    minion_pub = pki_dir / "minion" / "minion.pub"
    +    master_minions_dir = pki_dir / "master" / "minions"
    +    accepted_key = master_minions_dir / auth_minion_opts["id"]
    +
    +    # Copy minion public key to master's accepted keys
    +    with salt.utils.files.fopen(str(minion_pub)) as src:
    +        with salt.utils.files.fopen(str(accepted_key), "w") as dst:
    +            dst.write(src.read())
    +
    +    return accepted_key
    +
    +
    +@pytest.fixture
    +def req_server(auth_master_opts):
    +    """Create a ReqServerChannel instance for testing."""
    +    # Setup master secrets
    +    SMaster.secrets["aes"] = {
    +        "secret": multiprocessing.Array(
    +            ctypes.c_char,
    +            salt.utils.stringutils.to_bytes(salt.crypt.Crypticle.generate_key_string()),
    +        ),
    +        "reload": salt.crypt.Crypticle.generate_key_string,
    +    }
    +
    +    server_channel = server.ReqServerChannel.factory(auth_master_opts)
    +    server_channel.auto_key = salt.daemons.masterapi.AutoKey(auth_master_opts)
    +    server_channel.cache_cli = False
    +    server_channel.event = salt.utils.event.get_master_event(
    +        auth_master_opts, auth_master_opts["sock_dir"], listen=False
    +    )
    +    server_channel.master_key = salt.crypt.MasterKeys(auth_master_opts)
    +
    +    yield server_channel
    +
    +    server_channel.close()
    +    if "aes" in SMaster.secrets:
    +        SMaster.secrets.pop("aes")
    +
    +
    +async def test_auth_version_downgrade_v3_to_v0(
    +    pki_dir, auth_minion_opts, req_server, setup_accepted_minion
    +):
    +    """
    +    REGRESSION TEST: CVE-2025-62349 - Auth Version Downgrade Attack
    +
    +    Test that a minion cannot downgrade from version 3 to version 0
    +    to bypass security features like token validation.
    +
    +    EXPECTED BEHAVIOR:
    +    - Master should reject authentication attempts with version < 3
    +      from minions that previously authenticated with version 3
    +    - Or enforce version 3+ security features regardless of claimed version
    +
    +    CURRENT BEHAVIOR (VULNERABLE):
    +    - Test will FAIL - minion can successfully downgrade (vulnerability exists)
    +    - After fix is implemented, this test will PASS
    +    """
    +    # Read minion public key
    +    with salt.utils.files.fopen(str(pki_dir / "minion" / "minion.pub"), "r") as fp:
    +        pub_key = fp.read()
    +
    +    # Simulate version 0 auth load (no nonce, no signing)
    +    load_v0 = {
    +        "cmd": "_auth",
    +        "id": auth_minion_opts["id"],
    +        "pub": pub_key,
    +        "enc_algo": auth_minion_opts["encryption_algorithm"],
    +        "sig_algo": auth_minion_opts["signing_algorithm"],
    +    }
    +
    +    # Create payload for handle_message (version 0)
    +    payload = {"enc": "clear", "load": load_v0, "version": 0}
    +
    +    # Call handle_message which will enforce minimum_auth_version
    +    ret = await req_server.handle_message(payload)
    +
    +    # REGRESSION TEST: Version 0 should be REJECTED
    +    # With minimum_auth_version=3, version 0 should return "bad load"
    +    assert ret == "bad load", "Expected 'bad load' for rejected version 0 auth"
    +
    +
    +@pytest.mark.parametrize("downgrade_version", [0, 1, 2])
    +async def test_auth_version_downgrade_from_v3(
    +    pki_dir, auth_minion_opts, req_server, setup_accepted_minion, downgrade_version
    +):
    +    """
    +    REGRESSION TEST: CVE-2025-62349 - Auth Version Downgrade Attack (Parametrized)
    +
    +    Test that minions cannot downgrade to versions 0, 1, or 2 to bypass
    +    version 3 security features:
    +    - v0: No message signing, no nonce
    +    - v1: No message signing, no nonce
    +    - v2: Message signing but no token validation, no TTL checks, no ID matching
    +    - v3+: Full security (token validation, TTL, ID matching, session keys)
    +
    +    EXPECTED BEHAVIOR:
    +    - All downgrade attempts should be rejected
    +
    +    CURRENT BEHAVIOR (VULNERABLE):
    +    - Test will FAIL - downgrades are accepted, bypassing security
    +    - After fix is implemented, this test will PASS
    +    """
    +    with salt.utils.files.fopen(str(pki_dir / "minion" / "minion.pub"), "r") as fp:
    +        pub_key = fp.read()
    +
    +    load = {
    +        "cmd": "_auth",
    +        "id": auth_minion_opts["id"],
    +        "pub": pub_key,
    +        "enc_algo": auth_minion_opts["encryption_algorithm"],
    +        "sig_algo": auth_minion_opts["signing_algorithm"],
    +    }
    +
    +    # Add nonce for v2+ (but not for v0/v1)
    +    if downgrade_version >= 2:
    +        load["nonce"] = uuid.uuid4().hex
    +
    +    # Create payload for handle_message
    +    payload = {"enc": "clear", "load": load, "version": downgrade_version}
    +
    +    # Call handle_message which will enforce minimum_auth_version
    +    ret = await req_server.handle_message(payload)
    +
    +    # REGRESSION TEST: Old versions should be REJECTED
    +    # With minimum_auth_version=3, versions 0, 1, 2 should return "bad load"
    +    assert (
    +        ret == "bad load"
    +    ), f"Expected 'bad load' for rejected version {downgrade_version} auth"
    +
    +
    +def test_handle_message_version_extraction(auth_master_opts):
    +    """
    +    REGRESSION TEST: CVE-2025-62349 - Version Extraction from Untrusted Payload
    +
    +    Test that the master should have minimum_auth_version configured.
    +
    +    EXPECTED BEHAVIOR:
    +    - Master opts should have minimum_auth_version set
    +    - The commented code at salt/channel/server.py:144-145 should be enabled
    +
    +    CURRENT BEHAVIOR (VULNERABLE):
    +    - Test will FAIL - minimum_auth_version is not in opts
    +    - After fix is implemented, this test will PASS
    +    """
    +    # The current code at salt/channel/server.py:139-145 shows:
    +    # version = payload.get("version", 0)
    +    # #if version < self.opts["minimum_auth_version"]:
    +    # #    raise salt.ext.tornado.gen.Return("bad load")
    +
    +    # REGRESSION TEST: Verify minimum_auth_version exists in opts
    +    # Currently this will FAIL because the option doesn't exist
    +    assert (
    +        "minimum_auth_version" in auth_master_opts
    +    ), "Expected minimum_auth_version to be configured in master opts"
    +    assert (
    +        auth_master_opts["minimum_auth_version"] >= 3
    +    ), "Expected minimum auth version to be at least 3"
    +
    +
    +# Note: The remaining security bypasses (token, TTL, ID mismatch, session keys)
    +# are already tested via the parametrized downgrade tests above and the
    +# functional tests. The key regression test is ensuring old versions are rejected.
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.