VYPR
Medium severity5.3NVD Advisory· Published Feb 20, 2025· Updated Apr 15, 2026

CVE-2025-23020

CVE-2025-23020

Description

An issue was discovered in Kwik before 0.10.1. A hash collision vulnerability (in the hash table used to manage connections) allows remote attackers to cause a considerable CPU load on the server (a Hash DoS attack) by initiating connections with colliding Source Connection IDs (SCIDs).

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
tech.kwik:kwikMaven
< 0.10.10.10.1

Patches

2
d1288760c437
b0733d72bad7

use secure hash for ConnectionSource, the key in the connection registry hashtable

https://github.com/ptrd/kwikPeter DoornboschJan 10, 2025via ghsa
7 files changed · +148 11
  • buildSrc/src/main/groovy/buildlogic.java-common-conventions.gradle+3 0 modified
    @@ -22,6 +22,9 @@ javadoc {
     }
     
     repositories {
    +    flatDir {
    +        dirs "$rootDir/localdeps"
    +    }
         mavenCentral()
     }
     
    
  • core/build.gradle+2 1 modified
    @@ -2,12 +2,13 @@ plugins {
         id 'buildlogic.java-common-conventions'
     }
     
    -
     dependencies {
         implementation group: 'tech.kwik', name: 'agent15', version: "$agent15_version"
     
         // https://mvnrepository.com/artifact/at.favre.lib/hkdf
         implementation group: 'at.favre.lib', name: 'hkdf', version: '2.0.0'
    +
    +    implementation group: 'io.whitfin', name: 'siphash', version: '2.0.0-auto-module'
     }
     
     task includeVersion {
    
  • core/src/main/java/module-info.java+1 0 modified
    @@ -1,6 +1,7 @@
     module tech.kwik.core {
         requires tech.kwik.agent15;
         requires at.favre.lib.hkdf;
    +    requires io.whitfin.siphash;
     
         exports tech.kwik.core;
         exports tech.kwik.core.concurrent;
    
  • core/src/main/java/tech/kwik/core/server/impl/ConnectionSource.java+5 2 modified
    @@ -19,20 +19,23 @@
     package tech.kwik.core.server.impl;
     
     import tech.kwik.core.util.Bytes;
    +import tech.kwik.core.util.SecureHash;
     
     import java.util.Arrays;
     
     public class ConnectionSource {
     
         private final byte[] dcid;
    +    private final int hashCode;
     
    -    public ConnectionSource(byte[] dcid) {
    +    public ConnectionSource(byte[] dcid, SecureHash secureHash) {
             this.dcid = dcid;
    +        hashCode = secureHash.generateHashCode(dcid);
         }
     
         @Override
         public int hashCode() {
    -        return Arrays.hashCode(dcid);
    +        return hashCode;
         }
     
         @Override
    
  • core/src/main/java/tech/kwik/core/server/impl/ServerConnectionRegistryImpl.java+19 8 modified
    @@ -21,8 +21,10 @@
     import tech.kwik.core.log.Logger;
     import tech.kwik.core.server.ServerConnectionRegistry;
     import tech.kwik.core.util.Bytes;
    +import tech.kwik.core.util.SecureHash;
     
     import java.net.InetSocketAddress;
    +import java.security.SecureRandom;
     import java.time.Duration;
     import java.util.Comparator;
     import java.util.List;
    @@ -43,31 +45,40 @@ public class ServerConnectionRegistryImpl implements ServerConnectionRegistry {
         private final Map<ConnectionSource, ServerConnectionProxy> currentConnections;
         private final Lock checkEmptyLock;
         private final Condition allConnectionsClosed;
    +    private final SecureHash secureHash;
     
         ServerConnectionRegistryImpl(Logger log) {
             this.log = log;
             currentConnections = new ConcurrentHashMap<>();
     
             checkEmptyLock = new ReentrantLock();
             allConnectionsClosed = checkEmptyLock.newCondition();
    +
    +        byte[] seed = new byte[16];
    +        new SecureRandom().nextBytes(seed);
    +        secureHash = new SecureHash(seed);
         }
     
         @Override
         public void registerConnection(ServerConnectionProxy connection, byte[] connectionId) {
    -        currentConnections.put(new ConnectionSource(connectionId), connection);
    +        currentConnections.put(connectionSource(connectionId), connection);
    +    }
    +
    +    private ConnectionSource connectionSource(byte[] connectionId) {
    +        return new ConnectionSource(connectionId, secureHash);
         }
     
         @Override
         public void deregisterConnection(ServerConnectionProxy connection, byte[] connectionId) {
    -        currentConnections.remove(new ConnectionSource(connectionId));
    +        currentConnections.remove(connectionSource(connectionId));
             checkAllConnectionsClosed();
         }
     
         @Override
         public void registerAdditionalConnectionId(byte[] currentConnectionId, byte[] newConnectionId) {
    -        ServerConnectionProxy connection = currentConnections.get(new ConnectionSource(currentConnectionId));
    +        ServerConnectionProxy connection = currentConnections.get(connectionSource(currentConnectionId));
             if (connection != null) {
    -            currentConnections.put(new ConnectionSource(newConnectionId), connection);
    +            currentConnections.put(connectionSource(newConnectionId), connection);
             }
             else {
                 log.error("Cannot add additional cid to non-existing connection " + Bytes.bytesToHex(currentConnectionId));
    @@ -76,21 +87,21 @@ public void registerAdditionalConnectionId(byte[] currentConnectionId, byte[] ne
     
         @Override
         public void deregisterConnectionId(byte[] connectionId) {
    -        currentConnections.remove(new ConnectionSource(connectionId));
    +        currentConnections.remove(connectionSource(connectionId));
             checkAllConnectionsClosed();
         }
     
         Optional<ServerConnectionProxy> isExistingConnection(InetSocketAddress clientAddress, byte[] dcid) {
    -        return Optional.ofNullable(currentConnections.get(new ConnectionSource(dcid)));
    +        return Optional.ofNullable(currentConnections.get(connectionSource(dcid)));
         }
     
         ServerConnectionProxy removeConnection(ServerConnectionImpl connection) {
             // Remove the entry this is registered with the original dcid
    -        ServerConnectionProxy removed = currentConnections.remove(new ConnectionSource(connection.getOriginalDestinationConnectionId()));
    +        ServerConnectionProxy removed = currentConnections.remove(connectionSource(connection.getOriginalDestinationConnectionId()));
     
             // Remove all entries that are registered with the active cids
             List<ServerConnectionProxy> removedConnections = connection.getActiveConnectionIds().stream()
    -                .map(cid -> new ConnectionSource(cid))
    +                .map(cid -> connectionSource(cid))
                     .map(cs -> currentConnections.remove(cs))
                     .filter(Objects::nonNull)
                     .collect(Collectors.toList());
    
  • core/src/main/java/tech/kwik/core/util/SecureHash.java+36 0 added
    @@ -0,0 +1,36 @@
    +/*
    + * Copyright © 2025 Peter Doornbosch
    + *
    + * This file is part of Kwik, an implementation of the QUIC protocol in Java.
    + *
    + * Kwik is free software: you can redistribute it and/or modify it under
    + * the terms of the GNU Lesser General Public License as published by the
    + * Free Software Foundation, either version 3 of the License, or (at your option)
    + * any later version.
    + *
    + * Kwik is distributed in the hope that it will be useful, but
    + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
    + * more details.
    + *
    + * You should have received a copy of the GNU Lesser General Public License
    + * along with this program. If not, see <http://www.gnu.org/licenses/>.
    + */
    +package tech.kwik.core.util;
    +
    +import io.whitfin.siphash.SipHasher;
    +import io.whitfin.siphash.SipHasherContainer;
    +
    +public class SecureHash {
    +
    +    private final SipHasherContainer container;
    +
    +    public SecureHash(byte[] key) {
    +        container = SipHasher.container(key);
    +    }
    +
    +    public int generateHashCode(byte[] dcid) {
    +        long longHash = container.hash(dcid);
    +        return (int)(longHash ^ (longHash >>> 32));
    +    }
    +}
    
  • core/src/test/java/tech/kwik/core/util/SecureHashTest.java+82 0 added
    @@ -0,0 +1,82 @@
    +/*
    + * Copyright © 2025 Peter Doornbosch
    + *
    + * This file is part of Kwik, an implementation of the QUIC protocol in Java.
    + *
    + * Kwik is free software: you can redistribute it and/or modify it under
    + * the terms of the GNU Lesser General Public License as published by the
    + * Free Software Foundation, either version 3 of the License, or (at your option)
    + * any later version.
    + *
    + * Kwik is distributed in the hope that it will be useful, but
    + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
    + * more details.
    + *
    + * You should have received a copy of the GNU Lesser General Public License
    + * along with this program. If not, see <http://www.gnu.org/licenses/>.
    + */
    +package tech.kwik.core.util;
    +
    +import org.junit.jupiter.api.BeforeEach;
    +import org.junit.jupiter.api.Test;
    +import tech.kwik.core.test.ByteUtils;
    +
    +import java.security.SecureRandom;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +
    +
    +class SecureHashTest {
    +
    +    private SecureHash secureHash;
    +
    +    @BeforeEach
    +    void setUp() {
    +        byte[] key = "1234567890123456".getBytes();
    +        secureHash = new SecureHash(key);
    +    }
    +
    +    @Test
    +    void generatingHashForSameInputShouldReturnSameHash() {
    +        // Given
    +        byte[] input = ByteUtils.hexToBytes("cd8330cac6107e88");
    +
    +        // When
    +        int hash1 = secureHash.generateHashCode(input);
    +        int hash2 = secureHash.generateHashCode(input);
    +
    +        // Then
    +        assertThat(hash1).isEqualTo(hash2);
    +    }
    +
    +    @Test
    +    void generatingHashForDifferentInputsShouldReturnDifferentHashes() {
    +        // Given
    +        byte[] input1 = ByteUtils.hexToBytes("cd8330cac6107e88");
    +        byte[] input2 = ByteUtils.hexToBytes("07e89cd8330cac61");
    +        // When
    +        int hash1 = secureHash.generateHashCode(input1);
    +        int hash2 = secureHash.generateHashCode(input2);
    +
    +        // Then
    +        assertThat(hash1).isNotEqualTo(hash2);
    +    }
    +
    +    @Test
    +    void generatingHashForSameInputButDifferentSeedsShouldReturnDifferentHash() {
    +        // Given
    +        byte[] key = new byte[16];
    +        new SecureRandom().nextBytes(key);
    +        SecureHash secureHash2 = new SecureHash(key);
    +
    +        byte[] input = ByteUtils.hexToBytes("cd8330cac6107e88");
    +
    +        // When
    +        int hash1 = secureHash.generateHashCode(input);
    +        int hash2 = secureHash2.generateHashCode(input);
    +
    +        // Then
    +        assertThat(hash1).isNotEqualTo(hash2);
    +    }
    +}
    \ No newline at end of file
    

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

4

News mentions

0

No linked articles in our index yet.