Skip to content

Commit f9d1f46

Browse files
committed
Validate gRPC request content length
1 parent 66fdafe commit f9d1f46

2 files changed

Lines changed: 95 additions & 3 deletions

File tree

ldk-server-client/src/client.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ impl LdkServerClient {
436436
&self, request: &Rq, method: &str,
437437
) -> Result<Rs, LdkServerError> {
438438
let grpc_body = encode_grpc_frame(&request.encode_to_vec()).to_vec();
439+
let content_length = grpc_body.len().to_string();
439440

440441
let url = format!("https://{}{}{}", self.base_url, GRPC_SERVICE_PREFIX, method);
441442
let auth_header = self.compute_auth_header(&grpc_body);
@@ -444,6 +445,7 @@ impl LdkServerClient {
444445
.client
445446
.post(&url)
446447
.header("content-type", "application/grpc+proto")
448+
.header("content-length", content_length)
447449
.header("te", "trailers")
448450
.header("x-auth", auth_header)
449451
.body(grpc_body)
@@ -476,6 +478,7 @@ impl LdkServerClient {
476478
&self, request: &Rq, method: &str,
477479
) -> Result<GrpcStream<Rs>, LdkServerError> {
478480
let grpc_body = encode_grpc_frame(&request.encode_to_vec()).to_vec();
481+
let content_length = grpc_body.len().to_string();
479482

480483
let url = format!("https://{}{}{}", self.base_url, GRPC_SERVICE_PREFIX, method);
481484
let auth_header = self.compute_auth_header(&grpc_body);
@@ -486,6 +489,7 @@ impl LdkServerClient {
486489
HyperRequest::post(&url)
487490
.version(Version::HTTP_2)
488491
.header("content-type", "application/grpc+proto")
492+
.header("content-length", content_length)
489493
.header("te", "trailers")
490494
.header("x-auth", auth_header)
491495
.body(HyperBody::from(grpc_body))

ldk-server/src/service.rs

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::sync::Arc;
1414
use http_body_util::{BodyExt, Limited};
1515
use hyper::body::Incoming;
1616
use hyper::service::Service;
17-
use hyper::{Request, Response};
17+
use hyper::{HeaderMap, Request, Response};
1818
use ldk_node::bitcoin::hashes::hmac::{Hmac, HmacEngine};
1919
use ldk_node::bitcoin::hashes::{sha256, Hash, HashEngine};
2020
use ldk_node::Node;
@@ -256,7 +256,11 @@ impl Service<Request<Incoming>> for NodeService {
256256
let shutdown_rx = self.shutdown_rx.clone();
257257
let (request_parts, request_body) = req.into_parts();
258258
let future: Self::Future = Box::pin(async move {
259-
let body_bytes = match read_request_body(request_body).await {
259+
let content_length = match request_content_length(&request_parts.headers) {
260+
Ok(content_length) => content_length,
261+
Err(status) => return Ok(grpc_error_response(status)),
262+
};
263+
let body_bytes = match read_request_body(request_body, content_length).await {
260264
Ok(bytes) => bytes,
261265
Err(status) => return Ok(grpc_error_response(status)),
262266
};
@@ -499,7 +503,39 @@ async fn handle_grpc_unary<
499503
}
500504
}
501505

502-
async fn read_request_body(body: Incoming) -> Result<bytes::Bytes, GrpcStatus> {
506+
fn request_content_length(headers: &HeaderMap) -> Result<Option<u64>, GrpcStatus> {
507+
let Some(content_length) = headers.get("content-length") else {
508+
return Ok(None);
509+
};
510+
let len = content_length.to_str().ok().and_then(|value| value.parse::<u64>().ok()).ok_or_else(
511+
|| GrpcStatus::new(GRPC_STATUS_INVALID_ARGUMENT, "Invalid content-length header"),
512+
)?;
513+
if len > MAX_BODY_SIZE as u64 {
514+
return Err(GrpcStatus::new(
515+
GRPC_STATUS_INVALID_ARGUMENT,
516+
"Request body too large or failed to read",
517+
));
518+
}
519+
Ok(Some(len))
520+
}
521+
522+
fn validate_request_body_len(
523+
content_length: Option<u64>, actual_len: usize,
524+
) -> Result<(), GrpcStatus> {
525+
if let Some(expected_len) = content_length {
526+
if expected_len != actual_len as u64 {
527+
return Err(GrpcStatus::new(
528+
GRPC_STATUS_INVALID_ARGUMENT,
529+
"Request body length does not match content-length",
530+
));
531+
}
532+
}
533+
Ok(())
534+
}
535+
536+
async fn read_request_body(
537+
body: Incoming, content_length: Option<u64>,
538+
) -> Result<bytes::Bytes, GrpcStatus> {
503539
let limited_body = Limited::new(body, MAX_BODY_SIZE);
504540
let bytes = match limited_body.collect().await {
505541
Ok(collected) => collected.to_bytes(),
@@ -510,6 +546,7 @@ async fn read_request_body(body: Incoming) -> Result<bytes::Bytes, GrpcStatus> {
510546
));
511547
},
512548
};
549+
validate_request_body_len(content_length, bytes.len())?;
513550
Ok(bytes)
514551
}
515552

@@ -606,4 +643,55 @@ mod tests {
606643
assert!(result.is_err());
607644
assert_eq!(result.unwrap_err().error_code, LdkServerErrorCode::AuthError);
608645
}
646+
647+
#[test]
648+
fn test_request_content_length_missing() {
649+
let headers = HeaderMap::new();
650+
assert_eq!(request_content_length(&headers).unwrap(), None);
651+
}
652+
653+
#[test]
654+
fn test_request_content_length_parses_value() {
655+
let mut headers = HeaderMap::new();
656+
headers.insert("content-length", "42".parse().unwrap());
657+
658+
assert_eq!(request_content_length(&headers).unwrap(), Some(42));
659+
}
660+
661+
#[test]
662+
fn test_request_content_length_rejects_invalid_value() {
663+
let mut headers = HeaderMap::new();
664+
headers.insert("content-length", "not-a-number".parse().unwrap());
665+
666+
let err = request_content_length(&headers).unwrap_err();
667+
assert_eq!(err.code, GRPC_STATUS_INVALID_ARGUMENT);
668+
assert_eq!(err.message, "Invalid content-length header");
669+
}
670+
671+
#[test]
672+
fn test_request_content_length_rejects_oversized_value() {
673+
let mut headers = HeaderMap::new();
674+
headers.insert("content-length", (MAX_BODY_SIZE as u64 + 1).to_string().parse().unwrap());
675+
676+
let err = request_content_length(&headers).unwrap_err();
677+
assert_eq!(err.code, GRPC_STATUS_INVALID_ARGUMENT);
678+
assert_eq!(err.message, "Request body too large or failed to read");
679+
}
680+
681+
#[test]
682+
fn test_validate_request_body_len_allows_matching_length() {
683+
assert!(validate_request_body_len(Some(5), 5).is_ok());
684+
}
685+
686+
#[test]
687+
fn test_validate_request_body_len_allows_missing_length() {
688+
assert!(validate_request_body_len(None, 5).is_ok());
689+
}
690+
691+
#[test]
692+
fn test_validate_request_body_len_rejects_mismatch() {
693+
let err = validate_request_body_len(Some(6), 5).unwrap_err();
694+
assert_eq!(err.code, GRPC_STATUS_INVALID_ARGUMENT);
695+
assert_eq!(err.message, "Request body length does not match content-length");
696+
}
609697
}

0 commit comments

Comments
 (0)