Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 2 min read

A pre-start version guard for self-hosted services (after the LNbits 1.7.0 advisory)

On 8 November the LNbits team told operators to shut their servers down after attackers created unauthorized accounts and withdrew funds. One operator reported losing 2,285,545 sats. On 10 November, version 1.7.0 shipped

On 8 November the LNbits team told operators to shut their servers down after attackers created unauthorized accounts and withdrew funds. One operator reported losing 2,285,545 sats. On 10 November, version 1.7.0 shipped as an urgent security update, with a clear instruction: upgrade before you restart.

That instruction is easy to break by accident. A systemd unit restarts after a reboot, a container comes back with an old image, and the vulnerable version is serving requests again before anyone checks.

Here is a small guard you can put in front of any self-hosted service: it reads the installed version, compares it with a minimum you set, and exits non-zero if the version is too old, so the service never starts.

The script

// version-guard.mjs
// usage: node version-guard.mjs <path-to-pyproject.toml> <min-version>
import { readFileSync } from 'node:fs';

const [file, min] = process.argv.slice(2);
if (!file || !min) {
  console.error('usage: node version-guard.mjs <pyproject.toml> <min-version>');
  process.exit(64);
}

const text = readFileSync(file, 'utf8');
const m = text.match(/^version\s*=\s*"([^"]+)"/m);
if (!m) {
  console.error(`no version line found in ${file}`);
  process.exit(65);
}

const parse = (v) => v.replace(/^v/, '').split(/[.-]/).slice(0, 3).map((n) => parseInt(n, 10) || 0);
const cmp = (a, b) => {
  for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] - b[i];
  return 0;
};

const installed = m[1];
if (cmp(parse(installed), parse(min)) < 0) {
  console.error(`refusing to start: installed ${installed} < required ${min}`);
  process.exit(1);
}
console.log(`ok: installed ${installed} >= ${min}`);

Wire it into systemd

[Service]
ExecStartPre=/usr/bin/node /opt/guards/version-guard.mjs /opt/lnbits/pyproject.toml 1.7.0
ExecStart=/opt/lnbits/start.sh

If ExecStartPre fails, systemd doesn't run ExecStart. The unit fails, and you see it in systemctl status and your alerts.

Check it against the latest upstream release

To get told when upstream moves past your pin, compare with the GitHub releases API:

// latest-release.mjs
const res = await fetch('https://api.github.com/repos/lnbits/lnbits/releases/latest', {
  headers: { 'User-Agent': 'version-guard' },
});
const { tag_name } = await res.json();
console.log(tag_name);

Run it from cron and diff the output against your pinned minimum. A change means it's time to read the release notes.

Notes

  • The version parser handles 1.7.0, v1.7.0 and 1.7.0-rc1 (pre-release tags compare equal to the release; tighten it if that matters to you).
  • Point the guard at whatever file your install uses as the source of truth for its version.
  • A guard can only stop a restart. It does nothing for a server that is already running. When a project says "shut down now", shut it down first.

Sources: Bitcoin Magazine on LNbits 1.7.0, Crypto Briefing, Bitcoin.com News.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.