lightningd: don't return io logs from getlog
What changed, and why it matters
This commit fixes a security issue in Core Lightning's `getlog` JSON-RPC command. Previously, `getlog` could return 'io' level logs, which contain raw JSON-RPC and plugin traffic including secrets like authentication runes. Now `getlog` refuses to return io logs, keeping them only in local log files for users who explicitly enable them. This prevents a caller with RPC access from extracting sensitive secrets through the log command.
Treat this as a security hardening fix. Users and integrators should upgrade to a release containing this commit and review any custom tooling or plugins that previously relied on `getlog level=io`, replacing it with local log file access if needed. Audit RPC permissions to ensure only trusted parties can call `getlog`.
Security signals we found
Removal of sensitive log level from RPC API surface
New parameter validator explicitly rejects io log levels in getlog
Commit message states io logs can contain secrets such as runes
Test changed from xfail to active assertion that io logs are blocked
Changelog documents the security-relevant behavior change
Evidence from the diff
The patch removes the IO log level option from the getlog JSON-RPC command across the API surface (protobuf, Rust model, Python client, schemas, docs) and adds a new param_getloglevel validator in lightningd/log.c that rejects LOG_IO_IN and LOG_IO_OUT with an error message directing users to --log-level=io and the log file. The test test_getlog_no_io is updated from an expected failure (xfail) to a passing test, confirming the behavior. The change is a hardening fix to prevent secret leakage via RPC logs.
Changed components
lightningd/log.cdoc/schemas/getlog.jsoncln-grpc/proto/node.protocln-rpc/src/model.rscontrib/pyln-client/pyln/client/lightning.pycontrib/pyln-grpc-proto/pyln/grpc/node_pb2.pycontrib/msggen/msggen/schema.jsontests/test_misc.pyInspect captured patch +904 / −888
### cln-grpc/proto/node.proto
@@ -2191,7 +2191,6 @@ message GetlogRequest {
UNUSUAL = 1;
INFO = 2;
DEBUG = 3;
- IO = 4;
TRACE = 5;
}
optional GetlogLevel level = 1;
### cln-rpc/src/model.rs
@@ -2604,7 +2604,7 @@ pub mod requests {
"fundchannel_start"
}
}
- /// ['A string that represents the log level.']
+ /// ['A string that represents the log level. Note that *io* is not accepted here: the io log contains raw JSON-RPC and plugin traffic (including runes and other secrets), so it is only available in the log file, via *log-level=io*.']
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[allow(non_camel_case_types)]
pub enum GetlogLevel {
@@ -2616,8 +2616,6 @@ pub mod requests {
INFO = 2,
#[serde(rename = "debug")]
DEBUG = 3,
- #[serde(rename = "io")]
- IO = 4,
#[serde(rename = "trace")]
TRACE = 5,
}
@@ -2630,7 +2628,6 @@ pub mod requests {
1 => Ok(GetlogLevel::UNUSUAL),
2 => Ok(GetlogLevel::INFO),
3 => Ok(GetlogLevel::DEBUG),
- 4 => Ok(GetlogLevel::IO),
5 => Ok(GetlogLevel::TRACE),
o => Err(anyhow::anyhow!("Unknown variant {} for enum GetlogLevel", o)),
}
@@ -2645,7 +2642,6 @@ pub mod requests {
GetlogLevel::INFO => "INFO",
GetlogLevel::DEBUG => "DEBUG",
GetlogLevel::TRACE => "TRACE",
- GetlogLevel::IO => "IO",
}.to_string()
}
}
### contrib/msggen/msggen/schema.json
@@ -16679,11 +16679,10 @@
"unusual",
"info",
"debug",
- "trace",
- "io"
+ "trace"
],
"description": [
- "A string that represents the log level."
+ "A string that represents the log level. Note that *io* is not accepted here: the io log contains raw JSON-RPC and plugin traffic (including runes and other secrets), so it is only available in the log file, via *log-level=io*."
],
"default": "*info*"
}
### contrib/pyln-client/pyln/client/lightning.py
@@ -856,7 +856,7 @@ def getinfo(self):
def getlog(self, level=None):
"""
- Show logs, with optional log {level} (info|unusual|debug|io).
+ Show logs, with optional log {level} (info|unusual|debug|trace).
"""
payload = {
"level": level
### contrib/pyln-grpc-proto/pyln/grpc/node_pb2.py
[binary or diff unavailable]
### doc/schemas/getlog.json
@@ -20,11 +20,10 @@
"unusual",
"info",
"debug",
- "trace",
- "io"
+ "trace"
],
"description": [
- "A string that represents the log level."
+ "A string that represents the log level. Note that *io* is not accepted here: the io log contains raw JSON-RPC and plugin traffic (including runes and other secrets), so it is only available in the log file, via *log-level=io*."
],
"default": "*info*"
}
### lightningd/log.c
@@ -1223,6 +1223,30 @@ struct command_result *param_loglevel(struct command *cmd,
"'unusual'");
}
+/* The io log contains raw JSON-RPC and plugin traffic, which can contain
+ * secrets (such as runes), and getlog returns the entire log book: so we
+ * don't serve io here. It's still available in the log file, for those who
+ * run with --log-level=io. */
+static struct command_result *param_getloglevel(struct command *cmd,
+ const char *name,
+ const char *buffer,
+ const jsmntok_t *tok,
+ enum log_level **level)
+{
+ struct command_result *ret;
+
+ ret = param_loglevel(cmd, name, buffer, tok, level);
+ if (ret)
+ return ret;
+
+ if (**level == LOG_IO_IN || **level == LOG_IO_OUT)
+ return command_fail_badparam(cmd, name, buffer, tok,
+ "io logs are not available here:"
+ " use --log-level=io and read the"
+ " log file");
+ return NULL;
+}
+
static struct command_result *json_getlog(struct command *cmd,
const char *buffer,
const jsmntok_t *obj UNNEEDED,
@@ -1233,7 +1257,7 @@ static struct command_result *json_getlog(struct command *cmd,
struct log_book *log_book = cmd->ld->log_book;
if (!param(cmd, buffer, params,
- p_opt_def("level", param_loglevel, &minlevel, LOG_INFORM),
+ p_opt_def("level", param_getloglevel, &minlevel, LOG_INFORM),
NULL))
return command_param_failed();
### tests/test_misc.py
@@ -3895,11 +3895,10 @@ def test_getlog(node_factory):
logs = l1.rpc.getlog()['log']
assert [l for l in logs if l['type'] not in ("BROKEN", "UNUSUAL", "INFO")] == []
- logs = l1.rpc.getlog(level='io')['log']
- assert [l for l in logs if l['type'] not in ("BROKEN", "UNUSUAL", "INFO", "DEBUG", "TRACE", "IO_IN", "IO_OUT")] == []
+ logs = l1.rpc.getlog(level='trace')['log']
+ assert [l for l in logs if l['type'] not in ("BROKEN", "UNUSUAL", "INFO", "DEBUG", "TRACE")] == []
-@pytest.mark.xfail(strict=True)
def test_getlog_no_io(node_factory):
"""getlog must not hand out io logs: they contain the raw JSON-RPC and
plugin traffic, which includes secrets such as runes."""Why this scored 62/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.