mirror of
https://github.com/Ride-The-Lightning/RTL.git
synced 2026-08-13 12:33:07 +02:00
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".
This commit is contained in:
parent
09b29c3717
commit
b709c01277
4 changed files with 82 additions and 2 deletions
|
|
@ -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); });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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**
|
||||
([#TBD](https://github.com/Ride-The-Lightning/RTL/pull/TBD)).
|
||||
`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**
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
62
test/backend/eclair-channels.test.mjs
Normal file
62
test/backend/eclair-channels.test.mjs
Normal 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 });
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue