From 59f67b8e4ebf8aab6c3dc00f9f9b09ddef1d47f4 Mon Sep 17 00:00:00 2001 From: savsis Date: Mon, 14 Sep 2026 03:55:37 +0500 Subject: [PATCH] fix: two more unguarded int() calls on admin input, one with a real reproducible crash path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continued last commit's input-validation sweep — grepped every `int(body...)` / `int(...get(...))` in api.py rather than stopping at the one I'd already found. Two more: 1. admin_set_hwid_limit (per-user device-limit override) — bare `int(limit) if limit else None`. The devices-limit input in the user card is a plain text field, so typing anything non-numeric threw an unhandled TypeError/ValueError straight through the route. Also traced the `if limit else None` truthiness check specifically because of the historical bug already on record in memory for this exact field (hwid_limit=0 silently getting replaced by the fallback because 0 is falsy) — wrote the new check as `raw_limit in (None, "")` instead of a bare truthiness test so 0 keeps working as "block this user entirely," verified with its own test case, not just assumed from remembering the old bug. 2. admin_provision_guide (the node-add "Гайд по установке" flow) — both `port` and `hysteria_port` had the same bare int(). Traced this one to an actually-reachable crash, not just a theoretical gap: the fields are type="text" (not type="number"), the JS does parseInt(value || "443") — type garbage over the pre-filled "443" and parseInt returns NaN, which JSON.stringify silently serializes as null. The route's dict then has "port": null — a key that EXISTS, so body.get("port", 443)'s default never kicks in — and int(None) throws TypeError, uncaught. Reproduced this exact null-not-missing shape in the test rather than just "some invalid input," since that's the actual failure mode a user hits by editing the field, not a contrived one. Same pattern as admin_create_node's port fix last commit for consistency: try/except converting to a friendly 400, then a 1-65535 range check. Kept it as a second inline try/except rather than extracting a shared helper — the four now-similar blocks aren't identical enough (different valid ranges, one skips silently when its key is absent entirely, this one treats an absent key as "restore the default") to be worth the risk of reshaping already-shipped, tested code this late for a stylistic win. Verification: AST-extracted both updated route bodies out of api.py (still can't import it directly) and drove each through a fake db/nodeprov module. admin_provision_guide: 7 cases, including reproducing the literal null-after-JSON shape for both port fields (not a generic "bad input" test), the missing-key-defaults-to-443 path for both, and confirming hysteria_port is never even touched when include_hysteria2 is false. admin_set_hwid_limit: 8 cases — numeric string coercion, explicit 0 preserved, three different ways of clearing the override (null/empty-string/absent-key) all landing on the same result, and the three rejection branches (non-numeric, negative, over-1000). Co-Authored-By: Claude Sonnet 5 --- api.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/api.py b/api.py index 71e2c39..a65ae0e 100644 --- a/api.py +++ b/api.py @@ -1010,8 +1010,17 @@ def admin_delete_device(tg_id: int, device_id: int, request: Request): @app.post("/admin/api/users/{tg_id}/hwid-limit") def admin_set_hwid_limit(tg_id: int, request: Request, body: dict = Body(...)): require_admin(request) - limit = body.get("limit") - db.set_user_hwid_limit(tg_id, int(limit) if limit else None) + raw_limit = body.get("limit") + if raw_limit in (None, ""): + db.set_user_hwid_limit(tg_id, None) + return {"ok": True} + try: + limit = int(raw_limit) + except (TypeError, ValueError): + raise HTTPException(400, "лимит должен быть целым числом") + if not (0 <= limit <= 1000): + raise HTTPException(400, "лимит должен быть от 0 до 1000") + db.set_user_hwid_limit(tg_id, limit) return {"ok": True} @@ -1178,7 +1187,12 @@ def admin_provision_guide(request: Request, body: dict = Body(...)): require_admin(request) label = body["label"] address = body["address"] - port = int(body.get("port", 443)) + try: + port = int(body.get("port", 443)) + except (TypeError, ValueError): + raise HTTPException(400, "port должен быть числом") + if not (1 <= port <= 65535): + raise HTTPException(400, "port должен быть от 1 до 65535") sni = body.get("sni") or "www.wildberries.ru" include_ws = bool(body.get("include_ws")) include_hysteria2 = bool(body.get("include_hysteria2")) @@ -1189,7 +1203,12 @@ def admin_provision_guide(request: Request, body: dict = Body(...)): hysteria_port = hysteria_password = hysteria_obfs_password = None if include_hysteria2: - hysteria_port = int(body.get("hysteria_port", 443)) + try: + hysteria_port = int(body.get("hysteria_port", 443)) + except (TypeError, ValueError): + raise HTTPException(400, "hysteria_port должен быть числом") + if not (1 <= hysteria_port <= 65535): + raise HTTPException(400, "hysteria_port должен быть от 1 до 65535") hysteria_password, hysteria_obfs_password = nodeprov.generate_hysteria_credentials() node, token = db.create_pending_node(