Skip to content

Commit 1ce6b29

Browse files
committed
Bound unary gRPC response reads
Read unary client responses incrementally and reject bodies above the configured limit before protobuf decoding. Preallocate only after a valid Content-Length has been checked.
1 parent 28cec06 commit 1ce6b29

1 file changed

Lines changed: 60 additions & 4 deletions

File tree

ldk-server-client/src/client.rs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ type StreamingClient = HyperClient<HttpsConnector<hyper::client::HttpConnector>,
7373

7474
const GRPC_FRAME_HEADER_LEN: usize = 5;
7575

76+
// Applies to complete unary gRPC responses. The server applies the same cap to unary request
77+
// bodies before protobuf decoding.
78+
const MAX_GRPC_UNARY_RESPONSE_LEN: usize = 10 * 1024 * 1024;
79+
7680
// Applies to each server-streaming gRPC message. Graph RPCs use the unary client path and are not
7781
// constrained by this limit.
7882
const MAX_GRPC_STREAM_MESSAGE_LEN: usize = 4 * 1024 * 1024;
@@ -456,10 +460,7 @@ impl LdkServerClient {
456460
return Err(error);
457461
}
458462

459-
// Read the response body
460-
let payload = response.bytes().await.map_err(|e| {
461-
LdkServerError::new(InternalError, format!("Failed to read response body: {}", e))
462-
})?;
463+
let payload = read_grpc_unary_response_body(response).await?;
463464

464465
let proto_bytes = decode_grpc_body(&payload)
465466
.map_err(|e| LdkServerError::new(InternalError, e.message))?;
@@ -514,6 +515,42 @@ impl LdkServerClient {
514515
}
515516
}
516517

518+
async fn read_grpc_unary_response_body(
519+
mut response: reqwest::Response,
520+
) -> Result<Vec<u8>, LdkServerError> {
521+
let capacity = if let Some(content_length) = response.content_length() {
522+
check_grpc_unary_response_len(content_length)?;
523+
content_length as usize
524+
} else {
525+
0
526+
};
527+
528+
let mut payload = Vec::with_capacity(capacity);
529+
while let Some(chunk) = response.chunk().await.map_err(|e| {
530+
LdkServerError::new(InternalError, format!("Failed to read response body: {}", e))
531+
})? {
532+
let len = payload.len().checked_add(chunk.len()).ok_or_else(|| {
533+
LdkServerError::new(InternalError, "gRPC unary response body length overflow")
534+
})?;
535+
check_grpc_unary_response_len(len as u64)?;
536+
payload.extend_from_slice(&chunk);
537+
}
538+
Ok(payload)
539+
}
540+
541+
fn check_grpc_unary_response_len(len: u64) -> Result<(), LdkServerError> {
542+
if len > MAX_GRPC_UNARY_RESPONSE_LEN as u64 {
543+
return Err(LdkServerError::new(
544+
InternalError,
545+
format!(
546+
"gRPC unary response exceeds maximum size of {} bytes",
547+
MAX_GRPC_UNARY_RESPONSE_LEN
548+
),
549+
));
550+
}
551+
Ok(())
552+
}
553+
517554
/// Map a gRPC status code to an LdkServerError.
518555
fn grpc_code_to_error(code: u32, message: String) -> LdkServerError {
519556
match code {
@@ -721,6 +758,25 @@ mod tests {
721758
assert_eq!(err.message, "gRPC stream became unavailable: server shutting down");
722759
}
723760

761+
#[test]
762+
fn test_grpc_unary_response_len_allows_limit() {
763+
assert!(check_grpc_unary_response_len(MAX_GRPC_UNARY_RESPONSE_LEN as u64).is_ok());
764+
}
765+
766+
#[test]
767+
fn test_grpc_unary_response_len_rejects_oversized() {
768+
let err =
769+
check_grpc_unary_response_len(MAX_GRPC_UNARY_RESPONSE_LEN as u64 + 1).unwrap_err();
770+
assert_eq!(err.error_code, InternalError);
771+
assert_eq!(
772+
err.message,
773+
format!(
774+
"gRPC unary response exceeds maximum size of {} bytes",
775+
MAX_GRPC_UNARY_RESPONSE_LEN
776+
)
777+
);
778+
}
779+
724780
#[tokio::test]
725781
async fn test_event_stream_surfaces_terminal_grpc_status() {
726782
let (mut sender, body) = Body::channel();

0 commit comments

Comments
 (0)