Stop logging the eclair auth header at DEBUG level (#1664)

* Stop logging the eclair auth header at DEBUG level

getChannels in the eclair channels controller logged its whole request
options object. Eclair authenticates with HTTP basic auth, so those
options carry the configured lnApiPassword in an authorization header -
raising an eclair node's logLevel to DEBUG wrote
"authorization":"Basic <base64>" into the node log file, which is a
recoverable form of the credential and is routinely shared when
debugging.

The log now carries only the request url and form, matching every other
DEBUG log in the controllers. This was the only site in server/ passing a
whole options object to the logger; the rest log options.form, .url,
.body or .qs, none of which hold credentials.

Present since 0.12.0 and only reachable by opting in to DEBUG (the
default log level is ERROR), but it contradicted the logging guarantee
stated for #1659.

Found by scanning node logs at DEBUG while verifying the 0.15.10 branch
against the regtest fixture. Regression test added in
test/backend/eclair-channels.test.mjs; it fails on the previous code with
"auth header key must not reach the node log".

* Fill in PR number in release note (#1664)
This commit is contained in:
Suheb 2026-08-03 22:33:29 -07:00 committed by saubyk
parent cb1065e52b
commit 86d876b09e
No known key found for this signature in database
GPG key ID: 00C9E2BC2E45666F
4 changed files with 82 additions and 2 deletions

View file

@ -53,7 +53,10 @@ export const getChannels = (req, res, next) => {
options.form = req.query;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Channels Node Id', data: options.form });
}
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Options', data: options });
// Log the call's shape only. Eclair authenticates with HTTP basic auth, so the options
// object carries the node's lnApiPassword in its authorization header, and node logs are
// routinely shared when debugging.
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Options', data: { url: options.url, form: options.form } });
if (common.read_dummy_data) {
common.getDummyData('Channels', req.session.selectedNode.lnImplementation).then((data) => { res.status(200).json(data); });
}

View file

@ -19,6 +19,18 @@ this release should add its entry under the appropriate section below.
path. Adds regression coverage (`test/backend/common.test.mjs`). Users are encouraged
to update promptly.
- **Eclair: stop logging the node's auth header at DEBUG level**
([#1664](https://github.com/Ride-The-Lightning/RTL/pull/1664)).
`getChannels` in the Eclair channels controller logged its entire request options object,
which for Eclair carries HTTP basic auth — so raising an Eclair node's `logLevel` to
`DEBUG` wrote `authorization: Basic <base64>` into the node log, a recoverable form of the
configured `lnApiPassword`. The log now carries only the request url and form, matching
every other DEBUG log in the controllers. Present since 0.12.0 and only reachable by
opting in to `DEBUG` (the default level is `ERROR`), but it contradicted the logging
guarantee stated for #1659. Regression coverage added
(`test/backend/eclair-channels.test.mjs`). Found by auditing node logs at `DEBUG` while
verifying this release against the regtest fixture.
## Code Health
- **Bound remaining unbounded LND alias-resolution fan-outs**

View file

@ -55,7 +55,10 @@ export const getChannels = (req, res, next) => {
options.form = req.query;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Channels Node Id', data: options.form });
}
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Options', data: options });
// Log the call's shape only. Eclair authenticates with HTTP basic auth, so the options
// object carries the node's lnApiPassword in its authorization header, and node logs are
// routinely shared when debugging.
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Options', data: { url: options.url, form: options.form } });
if (common.read_dummy_data) {
common.getDummyData('Channels', req.session.selectedNode.lnImplementation).then((data) => { res.status(200).json(data); });
} else {

View file

@ -0,0 +1,62 @@
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { getChannels } from '../../backend/controllers/eclair/channels.js';
// Eclair authenticates with HTTP basic auth, so the request options carry the node's
// lnApiPassword in the authorization header. A DEBUG log of the whole options object
// therefore writes a recoverable credential into the node log file.
const buildRequest = (logFile) => ({
session: {
selectedNode: {
index: 1,
lnNode: 'eclair-node',
lnImplementation: 'ECL',
authentication: {
options: {
url: '',
rejectUnauthorized: false,
json: true,
headers: { authorization: 'Basic ' + Buffer.from(':super-secret-password').toString('base64') }
}
},
settings: { lnServerUrl: 'http://127.0.0.1:1/', logLevel: 'DEBUG', logFile: logFile }
}
},
query: {}
});
const waitForLog = async (logFile) => {
// logger.log appends asynchronously; give it a few turns to flush.
for (let i = 0; i < 40; i++) {
const contents = readFileSync(logFile, 'utf-8');
if (contents.includes('Channels =>')) { return contents; }
await new Promise((resolve) => setTimeout(resolve, 25));
}
return readFileSync(logFile, 'utf-8');
};
test('getChannels does not write the eclair auth header to the node log at DEBUG level', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'ecl-channels-'));
const logFile = join(tempDir, 'RTL-Node-1.log');
writeFileSync(logFile, '');
const req = buildRequest(logFile);
const res = { status: () => ({ json: () => { } }) };
try {
getChannels(req, res, () => { });
const contents = await waitForLog(logFile);
assert.ok(contents.includes('Channels =>'), 'expected the controller to have logged at DEBUG level');
assert.ok(!contents.includes('authorization'), 'auth header key must not reach the node log');
assert.ok(!contents.includes('super-secret-password'), 'lnApiPassword must not reach the node log');
assert.ok(!contents.includes(Buffer.from(':super-secret-password').toString('base64')), 'encoded credential must not reach the node log');
// The diagnostic value of the log — where the call went — is still there.
assert.ok(contents.includes('/channels'), 'request url should still be logged for diagnostics');
} finally {
rmSync(tempDir, { force: true, recursive: true });
}
});