lsp_plugin: use concrete type to avoid unwrap
What changed, and why it matters
This commit removes a runtime call to unwrap() in a Core Lightning plugin that handles incoming channel requests. Previously, if the JSON payload did not contain an 'openchannel' field, the plugin would panic and crash. The fix introduces a proper typed request structure so the plugin can gracefully handle malformed or unexpected input instead of crashing. It is a defensive hardening change rather than a fix for an active exploit.
Treat as a hardening/defensive fix. Review whether the plugin's hook registration and JSON schema assumptions are consistent with Core Lightning's openchannel hook payload, and verify that ok_or_continue! returns a valid JSON response for the hook on deserialization failure. No urgent security response is indicated absent evidence of active exploitation.
Security signals we found
Removal of unwrap() on user-controlled JSON input
Introduction of typed deserialization with graceful error handling
Potential denial-of-service via plugin crash from missing/malformed 'openchannel' field
Evidence from the diff
The on_openchannel hook in plugins/lsps-plugin/src/client.rs previously used v.get(“openchannel”).unwrap().clone(), which would panic if the openchannel key was missing. The patch replaces the ad-hoc local Request struct with a concrete OpenChannelRequest type imported from cln_lsps::lsps2::cln, and uses ok_or_continue! to return cleanly from the hook on deserialization failure. A new OpenChannelRequest/OpenChannelRequestOpenChannel struct is added in plugins/lsps-plugin/src/lsps2/cln.rs. The change prevents a plugin panic due to malformed hook payloads.
Changed components
plugins/lsps-plugin/src/client.rsplugins/lsps-plugin/src/lsps2/cln.rson_openchannel hook in the LSPS pluginInspect captured patch +37 / −13
diff --git a/plugins/lsps-plugin/src/client.rs b/plugins/lsps-plugin/src/client.rs
index 57104b30..0ca25e33 100644
--- a/plugins/lsps-plugin/src/client.rs
+++ b/plugins/lsps-plugin/src/client.rs
@@ -9,8 +9,8 @@ use cln_lsps::lsps0::{
};
use cln_lsps::lsps2::cln::tlv::encode_tu64;
use cln_lsps::lsps2::cln::{
- HtlcAcceptedRequest, HtlcAcceptedResponse, InvoicePaymentRequest, TLV_FORWARD_AMT,
- TLV_PAYMENT_SECRET,
+ HtlcAcceptedRequest, HtlcAcceptedResponse, InvoicePaymentRequest, OpenChannelRequest,
+ TLV_FORWARD_AMT, TLV_PAYMENT_SECRET,
};
use cln_lsps::lsps2::model::{
compute_opening_fee, Lsps2BuyRequest, Lsps2BuyResponse, Lsps2GetInfoRequest,
@@ -636,15 +636,11 @@ async fn on_openchannel(
p: cln_plugin::Plugin<State>,
v: serde_json::Value,
) -> Result<serde_json::Value, anyhow::Error> {
- #[derive(Deserialize)]
- struct Request {
- id: String,
- }
+ let req: OpenChannelRequest = ok_or_continue!(
+ serde_json::from_value(v).context("failed to deserialize open_channel request JSON")
+ );
- let req: Request = ok_or_continue!(serde_json::from_value(
- v.get("openchannel").unwrap().clone()
- )
- .context("failed to deserialize open_channel request JSON"));
+ // Fixme: Check that channel parameters are as negotiated.
let dir = p.configuration().lightning_dir;
let rpc_path = Path::new(&dir).join(&p.configuration().rpc_file);
@@ -656,7 +652,7 @@ async fn on_openchannel(
key: Some(vec![
"lsps".to_string(),
"client".to_string(),
- req.id.clone(),
+ req.openchannel.id.clone(),
]),
};
let ds_res = ok_or_continue!(cln_client
@@ -665,10 +661,17 @@ async fn on_openchannel(
.context("failed to get datastore record"));
if let Some(_rec) = ds_res.datastore.iter().next() {
- info!("Allowing zero-conf channel from LSP {}", &req.id);
+ info!(
+ "Allowing zero-conf channel from LSP {}",
+ &req.openchannel.id
+ );
let ds_req = DeldatastoreRequest {
generation: None,
- key: vec!["lsps".to_string(), "client".to_string(), req.id.clone()],
+ key: vec![
+ "lsps".to_string(),
+ "client".to_string(),
+ req.openchannel.id.clone(),
+ ],
};
if let Some(err) = cln_client.call_typed(&ds_req).await.err() {
// We can do nothing but report that there was an issue deleting the
diff --git a/plugins/lsps-plugin/src/lsps2/cln.rs b/plugins/lsps-plugin/src/lsps2/cln.rs
index 949409db..c2789c25 100644
--- a/plugins/lsps-plugin/src/lsps2/cln.rs
+++ b/plugins/lsps-plugin/src/lsps2/cln.rs
@@ -128,6 +128,27 @@ pub struct InvoicePaymentRequestPayment {
pub preimage: String,
pub msat: u64,
}
+
+#[derive(Debug, Deserialize)]
+pub struct OpenChannelRequest {
+ pub openchannel: OpenChannelRequestOpenChannel,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct OpenChannelRequestOpenChannel {
+ pub id: String,
+ pub funding_msat: u64,
+ pub push_msat: u64,
+ pub dust_limit_msat: u64,
+ pub max_htlc_value_in_flight_msat: u64,
+ pub channel_reserve_msat: u64,
+ pub htlc_minimum_msat: u64,
+ pub feerate_per_kw: u32,
+ pub to_self_delay: u32,
+ pub max_accepted_htlcs: u32,
+ pub channel_flags: u64,
+}
+
/// Deserializes a lowercase hex string to a `Vec<u8>`.
pub fn from_hex<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
Why this scored 34/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.