Refuse a conf write that widens a path to its own ancestor

Several scripts delete inside a conf path — the orphan cleaner runs rm -rf under a download dir
and rsync runs --delete against a destination — so /mnt/user/Movies becoming /mnt/user is the
edit that turns a cleanup into a sweep. Depth cannot be the test, because /tv and /movies are
real container-internal values here; direction can. Clearing a path, making it relative and
'..' segments go with it, and autofix additionally requires a proposed path to exist, since
every probe it has is a network probe and proves nothing about a directory. Refusals now reach
the caller: a save whose only change was refused answered ok with no explanation.
This commit is contained in:
Gmer4Lfe
2026-08-09 21:21:52 -04:00
parent 77fb1abb85
commit a5be719261
3 changed files with 112 additions and 5 deletions
+13 -2
View File
@@ -122,7 +122,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
$results = vv_conf_write_changes($changes);
$results = vv_conf_write_changes($changes, $rejected);
// Propagate master.conf to partner hosts when the owner edits it (mirrors rawconf.php).
$push = [];
@@ -131,7 +131,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
vv_push_setup_state();
}
echo json_encode(['ok' => !in_array(false, $results, true), 'files' => $results, 'push' => $push]);
// A refused change never reaches a file, so it leaves no false in $results — a save whose
// only change was refused used to answer ok:true and show the operator their old value back
// with no explanation. Refusals are failures here and they are named: the whole point of the
// path guard is that someone learns their edit would have widened a delete target.
echo json_encode(['ok' => !in_array(false, $results, true) && !$rejected,
'files' => $results,
'rejected' => $rejected,
'error' => $rejected
? 'Refused: ' . implode(', ',
array_map(fn($r) => $r['key'] . ' (' . $r['reason'] . ')', $rejected))
: null,
'push' => $push]);
exit;
}