fix: resolve 7 audit issues - webhook tokens, cookie security, avatar save, TUI fallback, dead code, valkey default

This commit is contained in:
2026-06-30 13:29:34 -04:00
parent 6f43e4f3a0
commit 2caedc172b
9 changed files with 41 additions and 103 deletions
+2 -2
View File
@@ -9,7 +9,7 @@ import (
// SetSessionCookie writes the session cookie with secure defaults.
// Shared by password login, registration, and WebAuthn login.
func SetSessionCookie(w http.ResponseWriter, cookieName, token string, duration time.Duration) {
secure := os.Getenv("COOKIE_SECURE") == "true"
secure := os.Getenv("COOKIE_SECURE") != "false"
http.SetCookie(w, &http.Cookie{
Name: cookieName,
@@ -17,7 +17,7 @@ func SetSessionCookie(w http.ResponseWriter, cookieName, token string, duration
Path: "/",
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteDefaultMode,
SameSite: http.SameSiteStrictMode,
MaxAge: int(duration.Seconds()),
})
}
+3 -2
View File
@@ -210,6 +210,7 @@ type loginRequest struct {
}
type updateProfileRequest struct {
AvatarURL string `json:"avatar_url"`
DisplayName string `json:"display_name"`
Bio string `json:"bio"`
AccentColor string `json:"accent_color"`
@@ -482,9 +483,9 @@ func (h *Handler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
_, err := h.db.ExecContext(r.Context(), `
UPDATE users
SET display_name = $2, bio = $3, accent_color = $4, status_text = $5, status = COALESCE(NULLIF($6, ''), status),
banner_url = NULLIF($7, ''), tagline = NULLIF($8, ''), pronouns = NULLIF($9, ''), social_links = $10
banner_url = NULLIF($7, ''), tagline = NULLIF($8, ''), pronouns = NULLIF($9, ''), social_links = $10, avatar = COALESCE(NULLIF($11, ''), avatar)
WHERE id = $1
`, userID, req.DisplayName, req.Bio, req.AccentColor, req.StatusText, req.Status, req.BannerURL, req.Tagline, req.Pronouns, socialLinksJSON)
`, userID, req.DisplayName, req.Bio, req.AccentColor, req.StatusText, req.Status, req.BannerURL, req.Tagline, req.Pronouns, socialLinksJSON, req.AvatarURL)
if err != nil {
http.Error(w, `{"error":"update failed"}`, http.StatusInternalServerError)
return
+1 -1
View File
@@ -108,7 +108,7 @@ func Load() *Config {
SSLMode: getEnv("POSTGRES_SSLMODE", "require"),
},
Valkey: ValkeyConfig{
URL: getEnv("VALKEY_URL", "redis://localhost:***@example.com"),
URL: getEnv("VALKEY_URL", "redis://localhost:6379"),
},
MinIO: MinIOConfig{
Endpoint: getEnv("MINIO_ENDPOINT", ""),
+3
View File
@@ -189,6 +189,9 @@ CREATE INDEX IF NOT EXISTS idx_webhooks_token ON webhooks(token);
ALTER TABLE webhooks ADD COLUMN IF NOT EXISTS token_hash VARCHAR(64);
CREATE INDEX IF NOT EXISTS idx_webhooks_token_hash ON webhooks(token_hash);
-- Drop plaintext token column — token_hash is the only stored credential
ALTER TABLE webhooks DROP COLUMN IF EXISTS token;
-- Ensure reply_to column exists on messages (added after initial table creation)
ALTER TABLE messages ADD COLUMN IF NOT EXISTS reply_to UUID REFERENCES messages(id) ON DELETE SET NULL;
-6
View File
@@ -28,12 +28,6 @@ type Handler struct {
logger *slog.Logger
}
type Handler_old struct {
db *sql.DB
hub *gateway.Hub
mentions *MentionHandler
}
func NewHandler(db *sql.DB, hub *gateway.Hub, pushHandler *push.Handler, logger *slog.Logger) *Handler {
return &Handler{
db: db,
-60
View File
@@ -5,66 +5,6 @@ import (
"database/sql"
)
// ComputeChannelPermissions returns effective permissions for a user in a specific channel,
// layering channel overrides on top of server-wide role permissions.
func (c *Checker) ComputeChannelPermissions(ctx context.Context, serverID, channelID, userID string) (int64, error) {
base, err := c.GetUserPermissions(ctx, serverID, userID)
if err != nil {
return 0, err
}
if Has(base, ADMINISTRATOR) {
return base, nil
}
// Everyone role override applies first.
var everyoneID string
_ = c.db.QueryRowContext(ctx, `SELECT id FROM roles WHERE server_id = $1 AND is_default = TRUE LIMIT 1`, serverID).Scan(&everyoneID)
if everyoneID != "" {
allow, deny, err := c.getOverride(ctx, channelID, "role", everyoneID)
if err != nil {
return 0, err
}
base |= allow
base &^= deny
}
// Member-specific role overrides.
rows, err := c.db.QueryContext(ctx, `
SELECT r.id FROM roles r
INNER JOIN member_roles mr ON mr.role_id = r.id
WHERE mr.user_id = $1 AND mr.server_id = $2
`, userID, serverID)
if err != nil {
return 0, err
}
defer rows.Close()
for rows.Next() {
var roleID string
if err := rows.Scan(&roleID); err != nil {
return 0, err
}
allow, deny, err := c.getOverride(ctx, channelID, "role", roleID)
if err != nil {
return 0, err
}
base |= allow
base &^= deny
}
if err := rows.Err(); err != nil {
return 0, err
}
// User-specific override wins last.
allow, deny, err := c.getOverride(ctx, channelID, "user", userID)
if err != nil {
return 0, err
}
base |= allow
base &^= deny
return base, nil
}
func (c *Checker) getOverride(ctx context.Context, channelID, targetType, targetID string) (allow, deny int64, err error) {
err = c.db.QueryRowContext(ctx, `
SELECT allow_bitflags, deny_bitflags FROM channel_overrides
+3 -3
View File
@@ -143,10 +143,10 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
var avatar sql.NullString
var createdAt sql.NullString
err = h.db.QueryRowContext(r.Context(), `
INSERT INTO webhooks (channel_id, name, token, token_hash, avatar, created_by)
VALUES ($1, $2, $3, $4, $5, $6)
INSERT INTO webhooks (channel_id, name, token_hash, avatar, created_by)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, channel_id, name, avatar, created_by, created_at::text
`, channelID, req.Name, token, tokenHash, req.Avatar, userID).Scan(
`, channelID, req.Name, tokenHash, req.Avatar, userID).Scan(
&wh.ID, &wh.ChannelID, &wh.Name, &avatar, &wh.CreatedBy, &createdAt,
)
if err != nil {