Files
autotest/DOCS/sonnet-app-autotest-review-v1.2.39.md
T

7.2 KiB
Raw Blame History

Code Review: app-autotest v1.2.39 — Соннет

Дата: 2026-08-03


🔴 Критические

1. XSS в instances.js — o.operation в innerHTML без _esc()

instances.js (функция toggleInstance):

opsEl.innerHTML = ops.map(o =>
  `<button ... onclick="runOp('${o.operation}',${o.svcOperationId})">${o.operation}</button>`
).join('');

o.operation вставляется три раза без _esc(): в onclick-атрибут ('${...}'), в текст кнопки (>${...}<). Если API вернёт operation = "'; alert(1)//" — onclick-атрибут ломается. На практике операции — это "modify"/"delete" из Nubes API, но принцип нарушен. _esc() определён специально для этого.


🟡 Важные

2. Двойной вызов detect_endpoint() при каждом HTTP-запросе

auth.py — get_client() вызывает detect_endpoint(token), и get_stand() вызывает detect_endpoint(token) независимо. В api_test.py при POST /api/test:

client = get_client()   # detect_endpoint() #1
...
get_client_id(), get_stand()   # detect_endpoint() #2

Итого 2 лишних HTTP-запроса к Nubes API (dev + test) при каждом запросе в облачном режиме. Оба блока if endpoint in STANDS срабатывают при стандартной конфигурации.

3. mode и polygon_stand не валидируются при set_mode

main.py:

new_mode = request.form.get("mode", "polygon")
new_stand = request.form.get("polygon_stand") or request.cookies.get("polygon_stand", "test")

Любое значение пишется в cookie. Если polygon_stand = "evil/../../../etc", то в get_client() формируется URL:

polygon_url.replace("/api/v1/svc", "/evil/../../../etc/api/v1/svc")

Путь нормализуется HTTP-клиентом/сервером. Допустимые значения известны: mode ∈ {"polygon","cloud"}, polygon_stand ∈ {"dev","test","prod"} — надо добавить whitelist.

4. В polygon-режиме у всех пользователей одинаковый client_id

auth.py:

def get_token():
    if get_mode() == "polygon":
        return current_app.config["NUBES_API_TOKEN"]  # env-токен

get_client_id() тоже парсит env-токен → у всех пользователей в эмуляции одинаковый ClientID. Вся изоляция данных в БД (runs, scenario_runs, scenario_definitions) — по (client_id, stand). Если два пользователя переключатся в polygon-режим — они видят историю и сценарии друг друга. Если приложение использует только один человек — не проблема. Если несколько — критично.

5. api_history — соединение не в finally

api_test.py:

try:
    conn = get_conn()
    cur = conn.cursor()
    cur.execute(...)
    ...
    cur.close()
    put_conn(conn)        # явный return соединения
    return jsonify(result)
except Exception as e:
    try: cur.close()      # NameError если исключение до cur = ...
    except: pass
    try: put_conn(conn)   # NameError если исключение до conn = ...
    except: pass
    return jsonify({"error": str(e)}), 500

Все остальные функции (api_scenario_run_status, api_scenario_status) используют finally: put_conn(conn) — корректный паттерн. Здесь — нет. При ошибке до cur.close() соединение возвращается, но cur не закрывается. Не утечка (psycopg2 закроет при возврате conn в пул), но несоответствие стилю.

6. stand_name() — подстрочный поиск

http_client.py:

for name in ("dev", "test"):
    if name in (endpoint or ""):
        return name

Если URL содержит "dev" как часть другого слова (например, "development", "devnull"), вернёт неверное значение. URL-то сейчас конкретные, но хрупко.


🟢 Рекомендации

7. Двойной отступ в scenario.py

scenario.py:

for i, step in enumerate(steps):
        step_num = i + 1  # 8 пробелов вместо 4

Весь цикл имеет нестандартный отступ (8 пробелов). Работает корректно (Python), но выглядит как след удалённого try: или with: блока, который был внутри for.

8. get_real_client() — мёртвый алиас

auth.py: функция идентична get_client(), есть только для обратной совместимости. В main.py и других файлах есть вызовы get_real_client(). Стоит постепенно заменить на get_client() и убрать алиас.

9. Нет UNIQUE на runs.op_uid

init_db.py: ручной UPSERT в save_run.py (UPDATE → INSERT) предполагает уникальность op_uid. Индекса нет. При маловероятной гонке двух параллельных save_run с одним op_uid — оба сделают UPDATE (rowcount=0) → оба сделают INSERT → дубль. Добавить CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_op_uid ON runs (op_uid) WHERE op_uid IS NOT NULL.

10. Ротация лога: flock(LOCK_UN) перед flush буфера

api_test.py:

f.write(rest)
fcntl.flock(f, fcntl.LOCK_UN)  # лок снят, но буфер ещё не сброшен

Python-буфер сбрасывается при закрытии файла (with-блок), но лок уже снят. Другой воркер может прочитать неполные данные. Добавить f.flush() перед LOCK_UN.


Итого по приоритетам

# Файл Проблема Серьёзность
1 static/js/instances.js o.operation в innerHTML без _esc() 🔴
2 api/auth.py Двойной detect_endpoint() на запрос 🟡
3 routes/main.py mode/polygon_stand без whitelist-валидации 🟡
4 api/auth.py Shared client_id в polygon-режиме 🟡
5 routes/api_test.py api_history без finally 🟡
6 api/http_client.py stand_name() substring match 🟡
7 operations/scenario.py Двойной отступ в for-цикле 🟢
8 api/auth.py get_real_client() мёртвый алиас 🟢
9 db/init_db.py Нет UNIQUE индекса на runs.op_uid 🟢
10 routes/api_test.py flock(LOCK_UN) перед flush в ротации 🟢