From 21843c7eb8caf2aa4159a89869f9007f9ccb46c5 Mon Sep 17 00:00:00 2001 From: Matthias Wiedemann Date: Thu, 22 Oct 2026 16:23:39 +0200 Subject: [PATCH] Limit remote maximum packet size announced by the server The remote maximum packet size from SSH_MSG_CHANNEL_OPEN_CONFIRMATION was stored without validation and later used to allocate Buffers, so a malicious server could trigger OutOfMemoryError or NegativeArraySizeException. Cap it at PACKET_MAX_SIZE - 4096, which is always protocol-compliant as it is only an upper bound for what we send. Also guard the SFTP pipe size computation against int overflow. Reported by pyj kor. Co-Authored-By: Claude Sonnet 5.5 --- src/main/java/com/jcraft/jsch/Channel.java | 8 ++++- .../java/com/jcraft/jsch/ChannelSftp.java | 3 +- .../jsch/ChannelRemotePacketSizeTest.java | 35 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/jcraft/jsch/ChannelRemotePacketSizeTest.java diff --git a/src/main/java/com/jcraft/jsch/Channel.java b/src/main/java/com/jcraft/jsch/Channel.java index 88bd6a98..f93a4728 100644 --- a/src/main/java/com/jcraft/jsch/Channel.java +++ b/src/main/java/com/jcraft/jsch/Channel.java @@ -54,6 +54,9 @@ public abstract class Channel { // discarded by the transport layer, killing the connection. private static final int MAX_LOCAL_PACKET_SIZE = Session.PACKET_MAX_SIZE - 4096; + // cap for the remote maximum packet size announced by the server, see setRemotePacketSize + private static final int MAX_REMOTE_PACKET_SIZE = Session.PACKET_MAX_SIZE - 4096; + int id; volatile int recipient = -1; protected byte[] type = Util.str2byte("foo"); @@ -487,7 +490,10 @@ synchronized void addRemoteWindowSize(long foo) { } void setRemotePacketSize(int foo) { - this.rmpsize = foo; + // The remote maximum packet size is attacker-controlled and is used to allocate Buffers. + // It is only an upper bound for what we may send, so capping it is always protocol-compliant. + // Negative values are unsigned values above Integer.MAX_VALUE. + this.rmpsize = (foo < 0 || foo > MAX_REMOTE_PACKET_SIZE) ? MAX_REMOTE_PACKET_SIZE : foo; } abstract void run(); diff --git a/src/main/java/com/jcraft/jsch/ChannelSftp.java b/src/main/java/com/jcraft/jsch/ChannelSftp.java index 23331e0b..6e085eb9 100644 --- a/src/main/java/com/jcraft/jsch/ChannelSftp.java +++ b/src/main/java/com/jcraft/jsch/ChannelSftp.java @@ -214,7 +214,8 @@ public void start() throws JSchException { PipedOutputStream pos = new PipedOutputStream(); io.setOutputStream(pos); - PipedInputStream pis = new MyPipedInputStream(pos, rq.size() * rmpsize); + PipedInputStream pis = new MyPipedInputStream(pos, + (int) Math.min((long) rq.size() * rmpsize, Integer.MAX_VALUE)); io.setInputStream(pis); io_in = io.in; diff --git a/src/test/java/com/jcraft/jsch/ChannelRemotePacketSizeTest.java b/src/test/java/com/jcraft/jsch/ChannelRemotePacketSizeTest.java new file mode 100644 index 00000000..7a65bdab --- /dev/null +++ b/src/test/java/com/jcraft/jsch/ChannelRemotePacketSizeTest.java @@ -0,0 +1,35 @@ +package com.jcraft.jsch; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ChannelRemotePacketSizeTest { + + private static final int MAX = Session.PACKET_MAX_SIZE - 4096; + + private static int apply(int size) { + Channel c = new ChannelSession(); + c.setRemotePacketSize(size); + return c.rmpsize; + } + + @Test + void normalValueIsKept() { + assertEquals(32768, apply(32768)); + assertEquals(MAX, apply(MAX)); + } + + @Test + void oversizedValueIsCapped() { + assertEquals(MAX, apply(MAX + 1)); + assertEquals(MAX, apply(67108864)); + assertEquals(MAX, apply(Integer.MAX_VALUE)); + } + + @Test + void negativeValueIsCapped() { + assertEquals(MAX, apply(-1)); + assertEquals(MAX, apply(Integer.MIN_VALUE)); + } +}