mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +00:00
Full codebase audit: 21 security/robustness fixes, 328 tests across 4 packages
Security & economy: - Credit()/Debit() reject non-positive amounts (closes infinite-money exploit) - Lottery ticket purchase uses in-transaction count check (closes TOCTOU race) - Arena bail channel ownership prevents double-close panic - Entry.ID validated before exec.Command in fetch-esteemed - Internal errors no longer leaked to users (forex, esteemed) Robustness: - safeGo() panic recovery added to 17 goroutine launch sites across 14 plugins - db.Close() added and called on shutdown - Miniflux mutex pattern fixed (snapshot-under-lock) - Silently ignored DB/JSON errors now logged (lottery_db) Performance: - Added indexes: idx_arena_runs_user(user_id, status), idx_euro_bal_user(user_id) UX: - Empty !buy args shows usage instead of "No item matching ''" - "Failed to load character" now suggests !adventure Test coverage: - internal/util/parser_test.go (19 tests: XP, levels, progress bar, commands, parsing) - internal/crypto/crypto_test.go (12 tests: encrypt/decrypt, HMAC, ParseKey) - internal/plugin/helpers_test.go (30 tests: formatNumber, calc, lottery, chat perks) - internal/plugin/audit_fixes_test.go (25 tests: safeGo, bail channels, error leaks, overflow) - internal/db/db_test.go (4 tests: Init, Close, schema indexes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
@@ -52,15 +53,19 @@ func lotteryCurrentWeekStart() string {
|
||||
func lotteryTicketCount(userID id.UserID, weekStart string) int {
|
||||
d := db.Get()
|
||||
var count int
|
||||
_ = d.QueryRow(`SELECT COUNT(*) FROM lottery_tickets WHERE user_id = ? AND week_start = ?`,
|
||||
string(userID), weekStart).Scan(&count)
|
||||
if err := d.QueryRow(`SELECT COUNT(*) FROM lottery_tickets WHERE user_id = ? AND week_start = ?`,
|
||||
string(userID), weekStart).Scan(&count); err != nil {
|
||||
slog.Error("lottery: ticket count query failed", "user", userID, "err", err)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func lotteryTotalTicketCount(weekStart string) int {
|
||||
d := db.Get()
|
||||
var count int
|
||||
_ = d.QueryRow(`SELECT COUNT(*) FROM lottery_tickets WHERE week_start = ?`, weekStart).Scan(&count)
|
||||
if err := d.QueryRow(`SELECT COUNT(*) FROM lottery_tickets WHERE week_start = ?`, weekStart).Scan(&count); err != nil {
|
||||
slog.Error("lottery: total ticket count query failed", "err", err)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -73,6 +78,16 @@ func lotteryInsertTickets(userID id.UserID, weekStart string, tickets [][]int) e
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Re-check ticket count inside transaction to prevent TOCTOU race.
|
||||
var existing int
|
||||
if err := tx.QueryRow(`SELECT COUNT(*) FROM lottery_tickets WHERE user_id = ? AND week_start = ?`,
|
||||
string(userID), weekStart).Scan(&existing); err != nil {
|
||||
return fmt.Errorf("lottery: count tickets in tx: %w", err)
|
||||
}
|
||||
if existing+len(tickets) > 100 {
|
||||
return fmt.Errorf("lottery: ticket limit exceeded (have %d, buying %d)", existing, len(tickets))
|
||||
}
|
||||
|
||||
for _, nums := range tickets {
|
||||
data, _ := json.Marshal(nums)
|
||||
_, err := tx.Exec(`INSERT INTO lottery_tickets (user_id, week_start, numbers) VALUES (?, ?, ?)`,
|
||||
@@ -167,7 +182,9 @@ func lotteryLoadHistory(limit int) ([]lotteryHistoryRow, error) {
|
||||
&h.PotTotal, &h.RolledOver); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = json.Unmarshal([]byte(winJSON), &h.WinningNumbers)
|
||||
if err := json.Unmarshal([]byte(winJSON), &h.WinningNumbers); err != nil {
|
||||
slog.Warn("lottery: corrupt winning_numbers JSON", "draw", h.DrawDate, "err", err)
|
||||
}
|
||||
history = append(history, h)
|
||||
}
|
||||
return history, rows.Err()
|
||||
@@ -200,7 +217,9 @@ func scanLotteryTickets(rows lotteryRows) ([]lotteryTicket, error) {
|
||||
if err := rows.Scan(&t.ID, &t.UserID, &t.WeekStart, &numsJSON, &matchCount, &prize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = json.Unmarshal([]byte(numsJSON), &t.Numbers)
|
||||
if err := json.Unmarshal([]byte(numsJSON), &t.Numbers); err != nil {
|
||||
slog.Warn("lottery: corrupt ticket numbers JSON", "id", t.ID, "err", err)
|
||||
}
|
||||
t.MatchCount = matchCount
|
||||
t.Prize = prize
|
||||
tickets = append(tickets, t)
|
||||
|
||||
Reference in New Issue
Block a user