Skip to content

Commit 0f35c82

Browse files
committed
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
1 parent 4dd93b9 commit 0f35c82

2 files changed

Lines changed: 92 additions & 9 deletions

File tree

sshd-common/src/main/java/org/apache/sshd/common/compression/CompressionZlib.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.zip.Deflater;
2424
import java.util.zip.Inflater;
2525

26+
import org.apache.sshd.common.SshConstants;
27+
import org.apache.sshd.common.SshException;
2628
import org.apache.sshd.common.util.buffer.Buffer;
2729

2830
/**
@@ -32,11 +34,13 @@
3234
*/
3335
public class CompressionZlib extends BaseCompression {
3436

37+
private static final int MAX_UNCOMPRESSED_SIZE = 8 * SshConstants.SSH_REQUIRED_PAYLOAD_PACKET_LENGTH_SUPPORT; // 256kB
38+
3539
private static final int BUF_SIZE = 4096;
3640

3741
private byte[] tmpbuf = new byte[BUF_SIZE];
38-
private Deflater compresser;
39-
private Inflater decompresser;
42+
private Deflater compressor;
43+
private Inflater decompressor;
4044

4145
/**
4246
* Create a new instance of a ZLib base compression
@@ -56,26 +60,31 @@ public boolean isDelayed() {
5660

5761
@Override
5862
public void init(Type type, int level) {
59-
compresser = new Deflater(level);
60-
decompresser = new Inflater();
63+
compressor = new Deflater(level);
64+
decompressor = new Inflater();
6165
}
6266

6367
@Override
6468
public void compress(Buffer buffer) throws IOException {
65-
compresser.setInput(buffer.array(), buffer.rpos(), buffer.available());
69+
compressor.setInput(buffer.array(), buffer.rpos(), buffer.available());
6670
buffer.wpos(buffer.rpos());
67-
for (int len = compresser.deflate(tmpbuf, 0, tmpbuf.length, Deflater.SYNC_FLUSH);
71+
for (int len = compressor.deflate(tmpbuf, 0, tmpbuf.length, Deflater.SYNC_FLUSH);
6872
len > 0;
69-
len = compresser.deflate(tmpbuf, 0, tmpbuf.length, Deflater.SYNC_FLUSH)) {
73+
len = compressor.deflate(tmpbuf, 0, tmpbuf.length, Deflater.SYNC_FLUSH)) {
7074
buffer.putRawBytes(tmpbuf, 0, len);
7175
}
7276
}
7377

7478
@Override
7579
public void uncompress(Buffer from, Buffer to) throws IOException {
76-
decompresser.setInput(from.array(), from.rpos(), from.available());
80+
decompressor.setInput(from.array(), from.rpos(), from.available());
81+
int start = to.wpos();
7782
try {
78-
for (int len = decompresser.inflate(tmpbuf); len > 0; len = decompresser.inflate(tmpbuf)) {
83+
for (int len = decompressor.inflate(tmpbuf); len > 0; len = decompressor.inflate(tmpbuf)) {
84+
if (to.wpos() + len - start > MAX_UNCOMPRESSED_SIZE) {
85+
throw new SshException(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR,
86+
"Compressed SSH packet inflated to more than 256kB");
87+
}
7988
to.putRawBytes(tmpbuf, 0, len);
8089
}
8190
} catch (DataFormatException e) {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.sshd.common.compression;
21+
22+
import java.io.IOException;
23+
24+
import org.apache.sshd.common.SshException;
25+
import org.apache.sshd.common.util.buffer.Buffer;
26+
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
27+
import org.apache.sshd.util.test.JUnitTestSupport;
28+
import org.junit.jupiter.api.MethodOrderer.MethodName;
29+
import org.junit.jupiter.api.Tag;
30+
import org.junit.jupiter.api.TestMethodOrder;
31+
import org.junit.jupiter.params.ParameterizedTest;
32+
import org.junit.jupiter.params.provider.ValueSource;
33+
34+
/**
35+
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
36+
*/
37+
@TestMethodOrder(MethodName.class)
38+
@Tag("NoIoTestCase")
39+
class CompresssionZlibTest extends JUnitTestSupport {
40+
41+
CompresssionZlibTest() {
42+
super();
43+
}
44+
45+
@ParameterizedTest
46+
@ValueSource(ints = { 1, 32, 255, 256, 257, 258, 259, 260, 261, 300 })
47+
void maxSize(int size) throws IOException {
48+
boolean expectSuccess = size <= 256;
49+
byte[] data = new byte[size * 1024];
50+
Compression comp = BuiltinCompressions.zlib.create();
51+
comp.init(Compression.Type.Deflater, 9);
52+
Buffer buf = new ByteArrayBuffer();
53+
buf.putRawBytes(data);
54+
assertEquals(data.length, buf.available());
55+
comp.compress(buf);
56+
assertTrue(buf.available() < data.length);
57+
assertTrue(buf.available() <= 256 * 1024);
58+
59+
Compression decompress = BuiltinCompressions.zlib.create();
60+
decompress.init(Compression.Type.Inflater, 9);
61+
Buffer dec = new ByteArrayBuffer();
62+
if (expectSuccess) {
63+
decompress.uncompress(buf, dec);
64+
assertEquals(data.length, dec.available());
65+
assertArrayEquals(data, dec.getCompactData());
66+
} else {
67+
assertThrows(SshException.class, () -> {
68+
decompress.uncompress(buf, dec);
69+
});
70+
assertTrue(dec.available() <= 256 * 1024);
71+
}
72+
}
73+
74+
}

0 commit comments

Comments
 (0)