What happened
On 10 November 2026 LNbits published v1.7.0, a critical security release. The project's notice says every version before 1.7.0 is affected and that funds on those versions can be stolen. Two days earlier it had asked operators to shut their servers down after a wave of attacks; Cointelegraph reported at least one operator losing about 0.023 BTC through newly created accounts.
The part that is easy to miss: credentials exposed before the upgrade may still give an attacker access afterwards. Upgrading closes the hole. It doesn't revoke keys that were read while it was open.
A version check you can run
The script below takes the version your instance reports, compares it with the 1.7.0 floor and with the latest release from the GitHub API, and prints the recovery steps from the LNbits notice. Node 18+ is enough, there are no dependencies, and it exits with 2 when the version is affected, so you can drop it into a cron job or CI.
// check-lnbits.mjs
const FLOOR = '1.7.0'; // every version before this is affected (LNbits, 10 Nov 2026)
const parse = (v) => {
const m = String(v).trim().replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?/);
if (!m) throw new Error(`not a version: ${v}`);
return {nums: [+m[1], +m[2], +m[3]], pre: m[4] || null};
};
// semver order, enough for LNbits tags: a pre-release sorts before the release
const cmp = (a, b) => {
const A = parse(a), B = parse(b);
for (let i = 0; i < 3; i++) if (A.nums[i] !== B.nums[i]) return A.nums[i] - B.nums[i];
if (A.pre && !B.pre) return -1;
if (!A.pre && B.pre) return 1;
return 0;
};
async function latestRelease() {
try {
const r = await fetch('https://api.github.com/repos/lnbits/lnbits/releases/latest', {
headers: {accept: 'application/vnd.github+json', 'user-agent': 'check-lnbits'},
});
if (!r.ok) return null;
const j = await r.json();
return {tag: j.tag_name, at: j.published_at};
} catch {
return null;
}
}
const mine = process.argv[2];
if (!mine) {
console.log('usage: node check-lnbits.mjs <your-version>');
process.exit(1);
}
const latest = await latestRelease();
const patched = cmp(mine, FLOOR) >= 0;
console.log(`your version ${mine}`);
console.log(`security floor ${FLOOR}`);
console.log(`latest release ${latest ? `${latest.tag} (${latest.at.slice(0, 10)})` : 'unavailable'}`);
console.log(patched
? 'PATCHED: keys an attacker saw before the upgrade still work until you replace them.'
: 'AFFECTED: stop the instance and upgrade to 1.7.0 or later.');
console.log(`
After the upgrade:
1. Suspect a compromise? Keep the logs, then move to a fresh server running 1.7.0.
2. Super Admin > Settings > Users > Reset Wallet Keys. Update connected apps.
3. Revoke and replace funding-source credentials (API keys, macaroons).
4. Rotate every other secret stored in LNbits settings.
5. Recreate extension connections (NWC Provider, LNDHub, Bolt Cards, Bleskomat, FOSSA).
`);
// set the code instead of calling process.exit() right after fetch (avoids a libuv assertion on Windows)
process.exitCode = patched ? 0 : 2;
Run against an old version, it reports the instance as affected and exits with 2:
$ node check-lnbits.mjs 1.6.2
your version 1.6.2
security floor 1.7.0
latest release v1.7.0 (2026-11-10)
AFFECTED: stop the instance and upgrade to 1.7.0 or later.
Why the comparison is hand-rolled
Comparing version strings as text fails at the first two-digit part: '1.10.0' < '1.7.0' is true in JavaScript. Splitting into numbers fixes that, and the pre-release rule makes 1.7.0-rc1 count as unpatched, which is the safe reading for a security floor.
The rotation is the real work
The script only tells you where you stand. The keys are the job: LNbits says an upgrade in place cannot remove access an attacker may already have, and it lists the five steps the script prints. Its notice also suggests keeping only day-to-day funds on the hot wallet and moving the rest to an on-chain wallet with offline keys.
Sources: LNbits, "LNbits 1.7.0: security update and upgrade guidance" (news.lnbits.com, 10 Nov 2026); LNbits v1.7.0 release on GitHub; Cointelegraph (11 Nov 2026).
Top comments (0)