reseller role is not enforced — delegated accounts reach the full Admin Control Plane
Affected: v1.4.0-LTS (main, 06ad79a)
Summary
README.md documents the role hierarchy as:
reseller: Delegated commercial management rights restricted to voucher redemption (/redeem), voucher batch generation (/mp_voucher create <cnt> <qta> <dys>), and voucher inventory auditing (/mp_voucher list). Destructive engine commands are automatically blocked with security violation logging.
None of that is enforced by the dispatcher.
_check_tg_role() can return superadmin, reseller, or none, but _process_cmd() only rejected none plus four superadmin-gated commands — a total of five role comparisons in the whole file. 18 of the 22 administrative commands were reachable by a reseller.
Impact
A reseller account — intended as a low-trust, voucher-only commercial role — instead obtains broad operational control of the proxy:
| Command |
Consequence |
/mp_link |
Returns proxy connection links and QR codes for secrets — i.e. working credentials |
/mp_secrets |
Enumerates every secret label with per-user connection and traffic stats |
/reply |
Sends an arbitrary message to an arbitrary chat id, labelled "Support Team Reply" — impersonation of the support team to any user, including the admin |
/mp_add |
Provisions new proxy accounts |
/mp_rotate, /mp_enable, /mp_disable |
Alters the state and keys of existing accounts |
/mp_setlimit |
Changes per-user quotas, connection and IP ceilings |
/mp_broadcast |
Sends a message to every user in bot_users.txt |
/mp_status, /mp_traffic, /mp_limits, /mp_upstreams, /mp_health, /mp_digest, /mp_fleet |
Reveals full operational and infrastructure detail |
Because a secret is the access credential for this proxy, /mp_link and /mp_secrets amount to credential disclosure, not merely information disclosure. /reply is a phishing primitive: a message arriving from the operator's own bot, labelled as the support team, is highly credible to an end user.
Secondary: unrecognised roles fail open
_check_tg_role() returns whatever admins.conf holds, unvalidated. The dispatcher only special-cased none and reseller, so any other value falls through into the admin case — for example a typo such as SUPERADMIN, or a stale value like operator. Such an account receives the same access as a reseller: nearly the full control plane, minus the four superadmin-gated commands (those gates are != "superadmin", so they correctly deny by default).
The CLI is not a vector for this. admin_add() normalises the role before writing:
case "$role" in superadmin|reseller) ;; *) role="reseller" ;; esac
and it is the only writer of admins.conf. So the fail-open requires a hand-edited admins.conf — which is a plain text file an operator is free to edit. The safe behaviour (refusing an unrecognised role) is the opposite of what currently happens.
Note this does not soften the primary bug: a coerced reseller already has the same near-total access. The allowlist below is the fix for both.
Secondary: the claimed violation logging does not exist
The README's "security violation logging" is not implemented anywhere — a grep for violation returns nothing in either the manager or the generated bot daemon. Additionally, the four superadmin-gate denials use tg_send(), which delivers the refusal to the admin's chat rather than to the sender, so the offending account gets no feedback at all.
Reproduction
mtproxymax admin add <telegram_id> reseller
- From that Telegram account, send
/mp_secrets (or /mp_link, or /reply <admin_chat_id> <text>) to the bot.
Expected: refusal, per the documented role.
Actual: the secret list is returned; links and QR codes are returned; the reply is delivered.
Method: validated by invoking _process_cmd() directly with the real _check_tg_role() and the real admins.conf on an Alpine v1.4.0-LTS box, stubbing only the outbound send functions. Since the authorisation decision lives entirely inside that function, this exercises the production code path; nothing was sent to Telegram.
Suggested fix
Make the control plane an explicit allowlist rather than two special-case denials, so any role value that is not superadmin or reseller is refused instead of granted access — deny-by-default for unrecognised roles.
resellerrole is not enforced — delegated accounts reach the full Admin Control PlaneAffected: v1.4.0-LTS (
main, 06ad79a)Summary
README.mddocuments the role hierarchy as:None of that is enforced by the dispatcher.
_check_tg_role()can returnsuperadmin,reseller, ornone, but_process_cmd()only rejectednoneplus foursuperadmin-gated commands — a total of five role comparisons in the whole file. 18 of the 22 administrative commands were reachable by areseller.Impact
A
reselleraccount — intended as a low-trust, voucher-only commercial role — instead obtains broad operational control of the proxy:/mp_link/mp_secrets/reply/mp_add/mp_rotate,/mp_enable,/mp_disable/mp_setlimit/mp_broadcastbot_users.txt/mp_status,/mp_traffic,/mp_limits,/mp_upstreams,/mp_health,/mp_digest,/mp_fleetBecause a secret is the access credential for this proxy,
/mp_linkand/mp_secretsamount to credential disclosure, not merely information disclosure./replyis a phishing primitive: a message arriving from the operator's own bot, labelled as the support team, is highly credible to an end user.Secondary: unrecognised roles fail open
_check_tg_role()returns whateveradmins.confholds, unvalidated. The dispatcher only special-casednoneandreseller, so any other value falls through into the admin case — for example a typo such asSUPERADMIN, or a stale value likeoperator. Such an account receives the same access as areseller: nearly the full control plane, minus the foursuperadmin-gated commands (those gates are!= "superadmin", so they correctly deny by default).The CLI is not a vector for this.
admin_add()normalises the role before writing:and it is the only writer of
admins.conf. So the fail-open requires a hand-editedadmins.conf— which is a plain text file an operator is free to edit. The safe behaviour (refusing an unrecognised role) is the opposite of what currently happens.Note this does not soften the primary bug: a coerced
reselleralready has the same near-total access. The allowlist below is the fix for both.Secondary: the claimed violation logging does not exist
The README's "security violation logging" is not implemented anywhere — a grep for
violationreturns nothing in either the manager or the generated bot daemon. Additionally, the foursuperadmin-gate denials usetg_send(), which delivers the refusal to the admin's chat rather than to the sender, so the offending account gets no feedback at all.Reproduction
mtproxymax admin add <telegram_id> reseller/mp_secrets(or/mp_link, or/reply <admin_chat_id> <text>) to the bot.Expected: refusal, per the documented role.
Actual: the secret list is returned; links and QR codes are returned; the reply is delivered.
Method: validated by invoking
_process_cmd()directly with the real_check_tg_role()and the realadmins.confon an Alpine v1.4.0-LTS box, stubbing only the outbound send functions. Since the authorisation decision lives entirely inside that function, this exercises the production code path; nothing was sent to Telegram.Suggested fix
Make the control plane an explicit allowlist rather than two special-case denials, so any role value that is not
superadminorreselleris refused instead of granted access — deny-by-default for unrecognised roles.