fix: close panics and races the audit's own fixes left nearby

Second-pass review of the 54-commit self-correcting audit. Each item below
was confirmed by reading the surrounding source (and, where practical, the
pre-fix code) before being changed; regression tests are included for every
behavioral fix.

Concurrency:
- eventbus: Bus.Subscribe called wg.Add with no synchronization against a
  concurrent Bus.Stop's wg.Wait, a real "WaitGroup misuse" panic risk (e.g. a
  Telegram-bot settings save racing panel shutdown/restart). Stop now flips a
  mu-guarded `stopped` flag before waiting, and Subscribe checks it under the
  same lock, so Add and Wait can no longer race.

Security:
- login_limiter: evictForRoom's fallback eviction picked an arbitrary map
  key, including ones still under an active cooldown - an attacker flooding
  /login with fresh usernames could evict their own (or anyone's) blocked
  record and reset the lockout. The fallback now skips actively-blocked
  records, only falling back to an unconditional evict if the map is
  somehow entirely full of active blocks (preserves the hard memory cap).

Subscription-endpoint panics (reachable by any client hitting /sub):
- internal/sub/service.go: applyPathAndHostParams/Obj (ws/httpupgrade/xhttp
  with no path settings object) and the TLS alpn readers in three places
  used unchecked type assertions - exactly the bug class abab7cd0 patched
  elsewhere in the same switch statements, just not these call sites.
- internal/sub/json_service.go, clash_service.go: the externalProxy loops
  in the JSON and Clash generators used unchecked assertions on a
  legacy/admin-supplied field (missing "port", non-object entry, etc.).
- internal/sub/json_service.go: realityData's shortId/serverName selection
  could assert a non-string array element.

Other correctness:
- client_traffic.go: ResetAllTraffics (touched by 3eb214d0) still skipped
  clearing NodeClientTraffic node-sync baselines, unlike its sibling reset
  paths in the same file - a node's next sync would re-add pre-reset delta
  on top of the freshly-zeroed counter.
- inbound_traffic.go: the traffic-tick tx's Commit/Rollback errors were
  silently discarded; now logged so a backend-level commit failure (e.g. an
  aborted Postgres tx from a best-effort helper) doesn't masquerade as a
  successful tick.
- outbound_subscription.go: the new subscriptionFetchClient doc comment was
  wedged between fetchAndStore's existing comment and fetchAndStore itself,
  leaving fetchAndStore undocumented and the comment describing the wrong
  function.

Convention cleanup:
- Removed narrative // comments added by the audit that violate this repo's
  no-inline-comment rule (mostly narrating the specific bug/fix rather than
  a lasting contract, and mostly on new Test functions, which this repo's
  existing tests never comment) - calibrated against this exact codebase's
  own pre-existing comment style so legitimate godoc-style doc comments
  were left alone.
This commit is contained in:
claude[bot]
2026-07-15 11:59:30 +00:00
parent a862680645
commit fbad71d620
35 changed files with 179 additions and 147 deletions
@@ -6,12 +6,6 @@ import (
"github.com/mymmrac/telego"
)
// A non-admin callback must never reach a privileged handler. The second
// callback switch runs outside the isAdmin guard, so without the default-deny
// check a non-admin who can tap an admin's inline button (e.g. in a group) could
// export the database backup or reset all traffic. Here the privileged handler
// would panic on the nil bot/services of a bare Tgbot; the guard must return
// first, so no panic occurs.
func TestAnswerCallbackDeniesPrivilegedActionToNonAdmin(t *testing.T) {
defer func() {
if r := recover(); r != nil {
@@ -2,10 +2,6 @@ package tgbot
import "testing"
// A transient "delete after N seconds" message must not reset the conversation
// state when its timer fires: the user may have advanced to the next wizard step
// (setting a fresh state) within that window, and clearing it would silently
// drop their next input.
func TestDeleteMessageAfterDelayKeepsUserState(t *testing.T) {
userStateMgr.reset()
t.Cleanup(userStateMgr.reset)
@@ -1128,11 +1128,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
}
}
// The callbacks below sit outside the isAdmin block above, so a non-admin who
// can see an admin's inline keyboard (for example when the bot runs in a
// group) could otherwise trigger a database backup export, a mass traffic
// reset or client creation. Default-deny: a non-admin may only run the
// per-user client_* callbacks that key off their own Telegram id.
if !isAdmin && !isClientSelfCallback(callbackQuery.Data) {
return
}