Route every conf writer through the guarded path

Eleven call sites wrote master.conf with tmp+rename and nothing else — no backup, no
parse check, no audit — including the two toggles the UI uses most and the raw editor
that installs a whole hand-edited file.
This commit is contained in:
Gmer4Lfe
2026-08-09 19:07:28 -04:00
parent d9f917ecef
commit 67eabdc17c
10 changed files with 174 additions and 121 deletions
+12 -1
View File
@@ -81,6 +81,7 @@
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
require_once dirname(__DIR__) . '/include/confform.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['ok' => false, 'error' => 'POST only']);
@@ -106,6 +107,9 @@ if (!file_exists($confPath)) {
}
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
// Captured before anything is rebuilt from it — this is what the write compares against to
// prove master.conf did not change while the move was being worked out.
$origConf = implode('', $lines ?: []);
if (!$lines) {
echo json_encode(['ok' => false, 'error' => 'Could not read master.conf']);
exit;
@@ -148,7 +152,14 @@ if ($toArray) {
$newLines = $resultLines;
}
if (!vv_write_conf_raw('master.conf', implode('', $newLines))) {
// The array was rebuilt from a copy read before the lock was taken. Handing vv_conf_edit() a
// closure that compares against the current contents turns that into an optimistic check: if
// anything changed master.conf in between, the move is abandoned rather than written over the
// top of it. Everything else — backup, bash -n, read-back, audit — comes with the shared path.
$ok = vv_conf_edit('master.conf', fn(string $cur): ?string =>
$cur === $origConf ? implode('', $newLines) : null, [], [$script]);
if (!$ok) {
echo json_encode(['ok' => false, 'error' => 'Write failed']);
exit;
}