v1.2.19: fixes from Codex audit — XSS in params, JS injection in onclick, scenario polling timeout, has_target validation, tracker atomic update

This commit is contained in:
2026-07-31 17:58:57 +04:00
parent e0e16b91a8
commit 58b823a260
4 changed files with 88 additions and 31 deletions
+55 -23
View File
@@ -113,36 +113,68 @@ def _locked_write(path, data):
os.close(fd)
def add(client_id, stand, instance_uid, svc_id, display_name):
"""Добавить инстанс в трекер (вызывается из executor сразу после create).
def _atomic_update(path, mutator):
"""Атомарно: открыть файл → заблокировать → прочитать → мутировать → записать.
Читает текущий файл → добавляет/обновляет запись → пишет обратно.
Если инстанс уже есть — перезаписывает (идемпотентность).
Вся операция под ОДНИМ lock, в отличие от старого подхода
_locked_read() + _locked_write() где между ними lock снимался.
Это предотвращает lost-update при конкурентных add/remove.
Args:
client_id: str — ClientID из JWT
stand: str — "dev"/"test"
instance_uid: str — UUID созданного инстанса
svc_id: int — ID сервиса
display_name: str — displayName инстанса"""
p = _path(client_id, stand)
data = _locked_read(p)
data[instance_uid] = {
"svcId": svc_id,
"displayName": display_name,
"instanceUid": instance_uid,
}
_locked_write(p, data)
path: str — путь к файлу
mutator: callable(data) — функция, мутирующая dict (возвращать не нужно)
Returns:
dict — результат после мутации (или {} если не смогли)"""
try:
fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o644)
except OSError:
return {}
try:
if not _acquire_lock(fd):
return {}
# Читаем
data = {}
try:
raw = os.read(fd, 65536)
if raw:
data = json.loads(raw.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError):
pass
# Мутируем
mutator(data)
# Пишем
os.lseek(fd, 0, 0)
os.ftruncate(fd, 0)
os.write(fd, json.dumps(data, indent=2).encode("utf-8"))
return data
finally:
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
def add(client_id, stand, instance_uid, svc_id, display_name):
"""Добавить инстанс в трекер (атомарно, под одним lock).
Читает → добавляет/обновляет запись → пишет — всё под эксклюзивной блокировкой.
Если инстанс уже есть — перезаписывает (идемпотентность)."""
def _add(data):
data[instance_uid] = {
"svcId": svc_id,
"displayName": display_name,
"instanceUid": instance_uid,
}
_atomic_update(_path(client_id, stand), _add)
def remove(client_id, stand, instance_uid):
"""Удалить инстанс из трекера (после успешного delete).
"""Удалить инстанс из трекера (атомарно, под одним lock).
pop с default=None — не падает если инстанса уже нет."""
def _rem(data):
data.pop(instance_uid, None)
_atomic_update(_path(client_id, stand), _rem)
pop с default=None — не падает если инстанса уже нет (уже удалён ранее)."""
p = _path(client_id, stand)
data = _locked_read(p)
data.pop(instance_uid, None)
_locked_write(p, data)
def list_all(client_id, stand):