fix(ssh/sftp): cap remote file reads on received bytes, not stat() size - #6168
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview
Unit tests in Reviewed by Cursor Bugbot for commit fb510d1. Configure here. |
The SFTP/SSH download routes buffered a remote file into memory with the only size guard being sftp.stat().size — a value the caller-supplied SSH server controls. A server that reports a tiny size and then streams endlessly drove unbounded heap growth until OOM. Read through the existing readNodeStreamToBufferWithLimit limiter, which enforces the cap on bytes actually received and destroys the stream on breach, via a small readSftpFileCapped wrapper in sftp/utils. All four SFTP-reading tool routes now share it — download, download-file, read-file-content, and the append path of write-file-content, which had no cap at all. The wrapper keeps a no-op error listener on the stream for its whole life: ssh2 rejects still-pending SFTP requests when the channel closes, which arrives as a late error event after the limiter has detached its own handlers, and an error event with no listener would take the process down. Too-large responses now return 413 to match how the rest of the routes surface PayloadSizeLimitError, and responses report the actual byte count rather than the server-reported stat size.
33cebd7 to
fb510d1
Compare
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit fb510d1. Configure here.
|
@greptile review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit fb510d1. Configure here.
|
@greptile review |
Greptile SummaryThe PR prevents caller-controlled SSH/SFTP servers from causing unbounded buffering by applying a shared limit to bytes actually received.
Confidence Score: 5/5The PR appears safe to merge, with the new limiter consistently preventing unbounded SSH/SFTP file buffering. The shared reader limits actual received bytes, destroys over-limit streams, safely absorbs documented late ssh2 errors, and all affected routes correctly surface cap violations without changing their successful response contracts.
|
| Filename | Overview |
|---|---|
| apps/sim/app/api/tools/sftp/utils.ts | Introduces the shared 50MB received-byte cap and preserves an error listener for late ssh2 channel failures. |
| apps/sim/app/api/tools/sftp/utils.test.ts | Covers normal reads, received-byte enforcement, stream destruction, the fixed cap, and late errors after abort. |
| apps/sim/app/api/tools/sftp/download/route.ts | Replaces unbounded chunk accumulation with the capped helper and maps size-limit failures to 413. |
| apps/sim/app/api/tools/ssh/download-file/route.ts | Applies capped buffering, returns actual received sizes, and surfaces limit failures as 413. |
| apps/sim/app/api/tools/ssh/read-file-content/route.ts | Consolidates the existing dynamic cap through the shared helper and reports actual buffered bytes. |
| apps/sim/app/api/tools/ssh/write-file-content/route.ts | Caps the existing-file read used by append mode while preserving prior handling of non-limit read errors. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[SSH/SFTP route] --> B[Remote stat pre-check]
B --> C[readSftpFileCapped]
C --> D[readNodeStreamToBufferWithLimit]
D -->|Within limit| E[Buffered response or append]
D -->|Received bytes exceed limit| F[Destroy stream]
F --> G[HTTP 413]
F -. late ssh2 error .-> H[Persistent no-op error listener]
Reviews (1): Last reviewed commit: "fix(ssh/sftp): cap remote file reads on ..." | Re-trigger Greptile
Summary
sftp.stat().size— a value the caller-supplied SSH server controls. A server that reports a tiny size and then streams endlessly drove unbounded heap growth until OOM.readNodeStreamToBufferWithLimitlimiter (the same one the S3/GCS/Blob providers use), which enforces the cap on bytes actually received and destroys the stream on breach, via a smallreadSftpFileCappedwrapper insftp/utils.sftp/download,ssh/download-file,ssh/read-file-content, and the append path ofssh/write-file-content, which had no cap at all.errorlistener on the stream for its whole life: ssh2 rejects still-pending SFTP requests when the channel closes, which arrives as a lateerrorevent after the limiter has detached its own handlers, and anerrorevent with no listener would take the process down.onedrive/download,latex, andv1/filessurfacePayloadSizeLimitError, and responses report the actual byte count rather than the server-reportedstatsize.Type of Change
Testing
app/api/tools/sftp/utils.test.ts, including a regression test for the late-error-after-abort crash. Verified each goes red when the corresponding guard is removed.ssh2SFTP server that reportssize: 1for/bomband then streams forever: the read aborts at ~50MB in ~120ms instead of never terminating, and the process exits cleanly. A legitimate file reads correctly, and a file exactly at the cap still succeeds.app/api/tools,tools/ssh,tools/sftp,lib/core/utils/stream-limitspass;tsc --noEmitclean;check:api-validation:strict,check:boundaries,check:client-boundary,check:utils,check:tool-registry-boundaryall pass.Checklist