#2688·paramiko

[BUG] - BufferedFile.read() re-slices the full buffer on every call, causing quadratic cost with small chunk sizes

Author: eGkritsisCreated Sep 10, 2026Updated Sep 10, 2026
LabelsBug

Are you using paramiko as a client or server?

Not sure

What feature(s) aren't working right?

SSH

What version(s) of paramiko are you using?

5.0.0

What version(s) of Python are you using?

3.12.3

What operating system and version are you using?

Ubuntu 24.04

If you're connecting as a client, which SSH server are you connecting to?

No response

If you're using paramiko as part of another tool, which tool/version?

No response

Expected/desired behavior

BufferedFile.read(size) should consume size bytes from the internal buffer in O(size) time, regardless of how many bytes remain buffered. Consuming from the front of the buffer should not copy the entire remainder.

Concretely, calling read(1) repeatedly on a buffered file of size N should take O(N) total time, not O(N²). The data returned to the caller must remain identical - only the internal buffer-management strategy should change.

Possible Fix

a fix that avoids re-slicing the full buffer (e.g., tracking an offset, or using a memoryview/collections.deque of chunks)

Actual behavior

Summary

paramiko.file.BufferedFile.read() trims its internal buffer with self._rbuffer = self._rbuffer[size:] on every call. Because slicing bytes copies data, repeatedly reading in small chunks from a large buffered file is O(n) per call and O(n²) overall. I understand from the security team that this is not considered a security vulnerability under paramiko's threat model (a remote SFTP server cannot control how a client chunks its read() calls), but they suggested filing it here as a performance issue.

Details

In paramiko/file.py, inside BufferedFile.read():

python
if size <= len(self._rbuffer):
    result = self._rbuffer[:size]
    self._rbuffer = self._rbuffer[size:]   # copies the whole remaining buffer
    self._pos += len(result)
    return result

For a file of size N, reading it in chunks of 1 byte causes N + (N-1) + … + 1 ≈ N²/2 bytes copied – quadratic instead of linear.

How to reproduce

PoC

python
import time, math
from paramiko.file import BufferedFile

class DummyFile(BufferedFile):
    def __init__(self, data):
        super().__init__()
        self._data = data
        self._pos = 0
        self._realpos = 0
        self._closed = False
        self._flags = self.FLAG_READ
        self._rbuffer = data   # pre-fill buffer with entire data
        self._pos = 0

    def _read(self, size):
        return b''   # no more data needed

sizes = [10000, 20000, 40000, 100000, 1000000, 2000000]
results = []
for N in sizes:
    data = b'A' * N
    f = DummyFile(data)
    t0 = time.time()
    for _ in range(N):
        f.read(1)
    elapsed = time.time() - t0
    print(f"N={N:>6} | read 1 byte N times | time {elapsed:.6f}s")
    results.append((N, elapsed))

xs = [math.log(n) for n, _ in results]
ys = [math.log(t) for _, t in results]
mx, my = sum(xs)/len(xs), sum(ys)/len(ys)
num = sum((x-mx)*(y-my) for x,y in zip(xs,ys))
den = sum((x-mx)**2 for x in xs)
slope = num/den
print(f"\nScaling exponent: k ~= {slope:.2f}")

Expected output:

N=  10000 | time 0.001680s
N=  20000 | time 0.004020s
N=  40000 | time 0.014845s
N= 100000 | time 0.081721s
N=1000000 | time 6.727586s
N=2000000 | time 38.769956s

Scaling exponent ≈ 1.91, confirming quadratic behavior.

Anything else?

Impact

CWE-407 (Algorithmic Complexity)

Credits

Evgenios Gkritsis, @eGkritsis (github) Vulnerability & Malware Researcher @ Athena Research Center & University of Piraeus