From 0505e96f70f9584c5f3fea154e664abe4fe0e74e Mon Sep 17 00:00:00 2001 From: Matteo Nastasi Date: Wed, 7 Oct 2026 09:23:57 +0200 Subject: [PATCH] answer the three refusals of the admin check alike userauth answered 402 for a session that does not exist, 403 for one that is not an admin's and 404 for a wrong hash. Whoever reaches the control channel could tell the three apart and so learn, session by session, which ones are alive and which belong to an admin. All three now answer 403 "denied". usermgmt.php, the only client, looks for 200 and nothing else; test/nonblocking.php does not read the reply. The reason is written to the daemon log, with the name and the ip of the user where there is one and never the session. A malformed command still answers 401: it says nothing about any session. Checked with the same harness as the previous commit: a good hash answers 200, an unknown session, a non-admin and a wrong hash all answer 403 "denied", each with its own line in the log. Co-Authored-By: Claude Opus 5.5 (1M context) --- web/Obj/sac-a-push.phh | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/web/Obj/sac-a-push.phh b/web/Obj/sac-a-push.phh index 24cd5df..6863f45 100644 --- a/web/Obj/sac-a-push.phh +++ b/web/Obj/sac-a-push.phh @@ -1468,23 +1468,30 @@ class Sac_a_push { if (!isset($cmd['sess']) || !isset($cmd['private'])) { return cmd_return(401, 'malformed cmd'); } + /* The reply goes back to whoever reached the control channel, so + the three refusals answer alike: telling them apart would let + anyone probing sessions learn which ones exist and which belong + to an admin. usermgmt.php only looks for 200. The reason goes + to the log, without the session, which is a credential, and + never with G_alarm_passwd, which is also the password of + admin.php and the key of the mail verification hashes. + hash_equals: a != stops at the first differing byte. */ $idx = -1; - if (($user = $this->app->get_user($cmd['sess'], $idx)) == FALSE) - return cmd_return(402, 'user not found'); - - if (($user->flags & USER_FLAG_TY_ADMIN) == 0x00) - return cmd_return(403, 'permission denied'); - - /* The reply goes back to whoever reached the control channel: - it must not carry G_alarm_passwd, which is also the password - of admin.php and the key of the mail verification hashes. - The failure goes to the log instead, without the session, - which is a credential too. hash_equals: a != stops at the - first differing byte. */ + if (($user = $this->app->get_user($cmd['sess'], $idx)) == FALSE) { + fprintf(STDERR, "DIRECT: userauth refused: no such session\n"); + return cmd_return(403, 'denied'); + } + + if (($user->flags & USER_FLAG_TY_ADMIN) == 0x00) { + fprintf(STDERR, "DIRECT: userauth refused: [%s] from [%s] is not an admin\n", + $user->name, $user->ip); + return cmd_return(403, 'denied'); + } + if (!hash_equals(md5($G_alarm_passwd.$user->ip.$user->sess), (string)$cmd['private'])) { - fprintf(STDERR, "DIRECT: userauth failed for admin [%s] from [%s]\n", + fprintf(STDERR, "DIRECT: userauth refused: wrong hash for admin [%s] from [%s]\n", $user->name, $user->ip); - return cmd_return(404, 'authentication failed'); + return cmd_return(403, 'denied'); } return cmd_return(200, 'success'); -- 2.47.3