fix(security): защита CGI агента, права конфига и захват лока

- агентский CGI: /diagnose мог удалять nft-таблицы, переписывать resolv.conf
  и перезапускать sing-box с любого устройства LAN без токена; bearer-токен
  теперь передаётся через окружение вместо argv (виден в ps), тело запроса
  ограничено 64KB до чтения, wildcard Access-Control-Allow-Origin убран -
  произвольные сайты больше не могут читать состояние роутера кросс-доменно
  (включая DNS rebinding)
- agent_api: сравнение токена за константное время
- uci/migration: /etc/config/tachyon хранит Telegram bot token и ссылки на
  подписки; UCI не сохраняет 0600 при коммитах, поэтому режим ужесточается
  после каждого коммита этого конфига и после миграций
- build.yml: убран NODE_TLS_REJECT_UNAUTHORIZED=0 из шагов публикации релиза -
  проверка сертификатов не должна отключаться на раннере, заливающем релизы
- initd/state: устранено окно TOCTOU при захвате устаревшего reload-лока:
  два ожидающих могли оба выполнить rm+rmdir+mkdir и каждый считал лок своим;
  захват теперь атомарным переименованием, которое выигрывает ровно один
  претендент
This commit is contained in:
Dushnilin
2026-08-21 19:19:40 +03:00
parent 8996790f25
commit 300ad50417
7 changed files with 106 additions and 26 deletions
+24 -8
View File
@@ -270,20 +270,36 @@ function acquire_runtime_dir_lock(lock_dir, owner_pid) {
let lock_stat = fs.stat(lock_dir + "/pid") || fs.stat(lock_dir);
let lock_age = (lock_stat && lock_stat.mtime) ? (int(clock()[0]) - lock_stat.mtime) : 9999;
let recorded_pid = first_line_value(lock_dir + "/pid");
if (pid_alive(first_line_value(lock_dir + "/pid")) && lock_age < 300)
if (pid_alive(recorded_pid) && lock_age < 300)
return false;
command_success_from_args([ "rm", "-f", lock_dir + "/pid" ]);
if (!command_success_from_args([ "rmdir", lock_dir ]))
return false;
if (!command_success_from_args([ "mkdir", lock_dir ]))
// pid file missing entirely: either another waiter is mid-takeover or a
// previous owner crashed before writing it. Only steal once the
// directory is old enough that a live holder cannot still be starting.
if (recorded_pid == "" && lock_age < 300)
return false;
if (lock_dir_write_owner(lock_dir, owner_pid))
return true;
// Takeover must be atomic: rm+rmdir+mkdir lets two concurrent waiters
// each believe they own the lock. Renaming the directory away succeeds
// for exactly one contender; the losers re-race on mkdir and either win
// a fresh lock or observe the winner's pid file.
let stale_dir = lock_dir + ".stale." + owner_pid + "." + int(clock()[0]);
if (!command_success_from_args([ "mv", lock_dir, stale_dir ]))
return false;
release_runtime_dir_lock(lock_dir);
command_success_from_args([ "rm", "-rf", stale_dir ]);
if (command_success_from_args([ "mkdir", lock_dir ])) {
if (lock_dir_write_owner(lock_dir, owner_pid))
return true;
release_runtime_dir_lock(lock_dir);
return false;
}
// Another contender re-created the lock between our rename and mkdir:
// they own it now.
return false;
}