fix: two more unguarded int() calls on admin input, one with a real reproducible crash path

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 <noreply@anthropic.com>
This commit is contained in:
Savsis? 2026-09-14 03:55:37 +05:00
parent f37b8a5018
commit 59f67b8e4e

23
api.py
View file

@ -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"]
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:
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(