clnrest: restrict maximum size of request bodies to 2MiB
What changed, and why it matters
This update fixes a bug in Core Lightning's built-in REST plugin (clnrest). Before the fix, anyone on the network could send a very large request to the REST API without logging in, causing the node to crash. The patch now refuses bodies larger than 2 MB and returns an error instead of trying to read unlimited data.
Upgrade to a release containing this commit. If running clnrest exposed to untrusted networks, treat this as a priority denial-of-service fix. Verify no other handlers read bodies with usize::MAX.
Security signals we found
Unauthenticated remote crash via oversized request body
Unbounded memory consumption in HTTP body handling
Changelog explicitly states security fix: 'an unauthenticated user could crash the node with a large request body'
Memory-exhaustion / denial-of-service vector
Evidence from the diff
The clnrest plugin previously called to_bytes(body.into_body(), usize::MAX), allowing an unauthenticated client to stream an unbounded request body. That could exhaust memory and crash the plugin/lightningd. The patch caps request bodies at 2 MiB via http_body_util’s length-limited body reader, adds a PayloadTooLarge AppError variant returning HTTP 413, and distinguishes LengthLimitError from other body-read failures.
Changed components
plugins/rest-plugin/src/handlers.rsplugins/rest-plugin/src/structs.rsclnrest REST pluginInspect captured patch +35 / −2
### Cargo.lock
@@ -577,6 +577,7 @@ dependencies = [
"cln-plugin",
"cln-rpc",
"futures-util",
+ "http-body-util",
"hyper 1.11.0",
"log",
"log-panics",
### plugins/rest-plugin/Cargo.toml
@@ -41,6 +41,7 @@ futures-util = { version = "0.3", default-features = false, features = [
] }
rcgen = "0.14"
hyper = "1"
+http-body-util = "0.1"
tower = "0.5"
tower-http = { version = "0.7", features = ["cors", "set-header"] }
utoipa = { version = "5", features = ['axum_extras'] }
### plugins/rest-plugin/src/handlers.rs
@@ -23,6 +23,10 @@ use crate::{
structs::{AppError, CheckRuneParams, ClnrestMap, PluginState},
};
+/// Maximum size (in bytes) of an accepted request body.
+/// Oversized bodies are rejected with 413.
+pub const MAX_BODY_SIZE: usize = 2 * 1024 * 1024; // 2 MiB
+
/* Handler for list-methods */
#[utoipa::path(
get,
@@ -115,11 +119,20 @@ pub async fn call_rpc_method(
.and_then(|v| v.to_str().ok())
.map(String::from);
- let request_bytes = match to_bytes(body.into_body(), usize::MAX).await {
+ let request_bytes = match to_bytes(body.into_body(), MAX_BODY_SIZE).await {
Ok(o) => o,
Err(e) => {
+ if is_body_too_large(&e) {
+ return Err(AppError::PayloadTooLarge(RpcError {
+ code: Some(-32600),
+ data: None,
+ message: format!(
+ "Request body exceeds the maximum allowed size of {MAX_BODY_SIZE} bytes"
+ ),
+ }));
+ }
return Err(AppError::InternalServerError(RpcError {
- code: None,
+ code: Some(-32700),
data: None,
message: format!("Could not read request body: {}", e),
}));
@@ -160,6 +173,21 @@ pub async fn call_rpc_method(
convert_json_to_response(headers, &rest_map.rpc_method, cln_result)
}
+fn is_body_too_large(e: &axum::Error) -> bool {
+ fn inner(e: &(dyn std::error::Error + 'static)) -> bool {
+ if e.downcast_ref::<http_body_util::LengthLimitError>()
+ .is_some()
+ {
+ return true;
+ }
+ match e.source() {
+ Some(src) => inner(src),
+ None => false,
+ }
+ }
+ inner(e)
+}
+
fn fill_rune_restrictions(
rest_map: &mut ClnrestMap,
rpc_params: &serde_json::Map<String, serde_json::Value>,
### plugins/rest-plugin/src/structs.rs
@@ -31,6 +31,7 @@ pub enum AppError {
MethodNotAllowed(RpcError),
InternalServerError(RpcError),
NotAcceptable(RpcError),
+ PayloadTooLarge(RpcError),
}
impl IntoResponse for AppError {
@@ -42,6 +43,7 @@ impl IntoResponse for AppError {
AppError::MethodNotAllowed(err) => (StatusCode::METHOD_NOT_ALLOWED, err),
AppError::InternalServerError(err) => (StatusCode::INTERNAL_SERVER_ERROR, err),
AppError::NotAcceptable(err) => (StatusCode::NOT_ACCEPTABLE, err),
+ AppError::PayloadTooLarge(err) => (StatusCode::PAYLOAD_TOO_LARGE, err),
};
let body = Json(json!(error_message));
@@ -58,6 +60,7 @@ impl std::fmt::Display for AppError {
AppError::MethodNotAllowed(err) => write!(f, "Method not allowed: {err}"),
AppError::InternalServerError(err) => write!(f, "Internal Server Error: {err}"),
AppError::NotAcceptable(err) => write!(f, "Not Acceptable: {err}"),
+ AppError::PayloadTooLarge(err) => write!(f, "Payload Too Large: {err}"),
}
}
}Why this scored 74/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.