This is an automated email from the ASF dual-hosted git repository. twolf pushed a commit to branch dev_3.0 in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit 324149d6425ddcf102ca45496c1992214edb0a00 Author: Thomas Wolf <[email protected]> AuthorDate: Wed Apr 1 19:41:56 2026 +0200 Limit the size of SSH packets after decompression A malicious peer might send a packet that passes the initial size checks but then inflates to a huge amount of data. Limit the size of the packet after compression to 256kB. This is fine by RFC 4253.[1] [1] https://datatracker.ietf.org/doc/html/rfc4253#section-6.1 --- .../sshd/common/compression/CompressionZlib.java | 27 +++++--- .../common/compression/CompresssionZlibTest.java | 74 ++++++++++++++++++++++ 2 files changed, 92 insertions(+), 9 deletions(-) diff --git a/sshd-common/src/main/java/org/apache/sshd/common/compression/CompressionZlib.java b/sshd-common/src/main/java/org/apache/sshd/common/compression/CompressionZlib.java index 2f5eb222b..12d5751ee 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/compression/CompressionZlib.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/compression/CompressionZlib.java @@ -23,6 +23,8 @@ import java.util.zip.DataFormatException; import java.util.zip.Deflater; import java.util.zip.Inflater; +import org.apache.sshd.common.SshConstants; +import org.apache.sshd.common.SshException; import org.apache.sshd.common.util.buffer.Buffer; /** @@ -32,11 +34,13 @@ import org.apache.sshd.common.util.buffer.Buffer; */ public class CompressionZlib extends BaseCompression { + private static final int MAX_UNCOMPRESSED_SIZE = 8 * SshConstants.SSH_REQUIRED_PAYLOAD_PACKET_LENGTH_SUPPORT; // 256kB + private static final int BUF_SIZE = 4096; private byte[] tmpbuf = new byte[BUF_SIZE]; - private Deflater compresser; - private Inflater decompresser; + private Deflater compressor; + private Inflater decompressor; /** * Create a new instance of a ZLib base compression @@ -56,26 +60,31 @@ public class CompressionZlib extends BaseCompression { @Override public void init(Type type, int level) { - compresser = new Deflater(level); - decompresser = new Inflater(); + compressor = new Deflater(level); + decompressor = new Inflater(); } @Override public void compress(Buffer buffer) throws IOException { - compresser.setInput(buffer.array(), buffer.rpos(), buffer.available()); + compressor.setInput(buffer.array(), buffer.rpos(), buffer.available()); buffer.wpos(buffer.rpos()); - for (int len = compresser.deflate(tmpbuf, 0, tmpbuf.length, Deflater.SYNC_FLUSH); + for (int len = compressor.deflate(tmpbuf, 0, tmpbuf.length, Deflater.SYNC_FLUSH); len > 0; - len = compresser.deflate(tmpbuf, 0, tmpbuf.length, Deflater.SYNC_FLUSH)) { + len = compressor.deflate(tmpbuf, 0, tmpbuf.length, Deflater.SYNC_FLUSH)) { buffer.putRawBytes(tmpbuf, 0, len); } } @Override public void uncompress(Buffer from, Buffer to) throws IOException { - decompresser.setInput(from.array(), from.rpos(), from.available()); + decompressor.setInput(from.array(), from.rpos(), from.available()); + int start = to.wpos(); try { - for (int len = decompresser.inflate(tmpbuf); len > 0; len = decompresser.inflate(tmpbuf)) { + for (int len = decompressor.inflate(tmpbuf); len > 0; len = decompressor.inflate(tmpbuf)) { + if (to.wpos() + len - start > MAX_UNCOMPRESSED_SIZE) { + throw new SshException(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, + "Compressed SSH packet inflated to more than 256kB"); + } to.putRawBytes(tmpbuf, 0, len); } } catch (DataFormatException e) { diff --git a/sshd-common/src/test/java/org/apache/sshd/common/compression/CompresssionZlibTest.java b/sshd-common/src/test/java/org/apache/sshd/common/compression/CompresssionZlibTest.java new file mode 100644 index 000000000..a563b3e37 --- /dev/null +++ b/sshd-common/src/test/java/org/apache/sshd/common/compression/CompresssionZlibTest.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.common.compression; + +import java.io.IOException; + +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.apache.sshd.util.test.JUnitTestSupport; +import org.junit.jupiter.api.MethodOrderer.MethodName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@TestMethodOrder(MethodName.class) +@Tag("NoIoTestCase") +class CompresssionZlibTest extends JUnitTestSupport { + + CompresssionZlibTest() { + super(); + } + + @ParameterizedTest + @ValueSource(ints = { 1, 32, 255, 256, 257, 258, 259, 260, 261, 300 }) + void maxSize(int size) throws IOException { + boolean expectSuccess = size <= 256; + byte[] data = new byte[size * 1024]; + Compression comp = BuiltinCompressions.zlib.create(); + comp.init(Compression.Type.Deflater, 9); + Buffer buf = new ByteArrayBuffer(); + buf.putRawBytes(data); + assertEquals(data.length, buf.available()); + comp.compress(buf); + assertTrue(buf.available() < data.length); + assertTrue(buf.available() <= 256 * 1024); + + Compression decompress = BuiltinCompressions.zlib.create(); + decompress.init(Compression.Type.Inflater, 9); + Buffer dec = new ByteArrayBuffer(); + if (expectSuccess) { + decompress.uncompress(buf, dec); + assertEquals(data.length, dec.available()); + assertArrayEquals(data, dec.getCompactData()); + } else { + assertThrows(SshException.class, () -> { + decompress.uncompress(buf, dec); + }); + assertTrue(dec.available() <= 256 * 1024); + } + } + +}
