security: remediate all P0-P2 audit findings (12 tasks)
P0 fixes: - WebSocket origin checking (reject untrusted origins) - WS session token moved from URL query param to first message frame - WebAuthn login cookie now uses Secure flag via shared SetSessionCookie - PostgreSQL sslmode configurable via POSTGRES_SSLMODE env (default: require) - Fixed DatabaseDSN to use real password instead of masked placeholder P1 fixes: - Per-IP rate limiting middleware (auth: 5 req/s, invites: 2 req/s) - Security headers on all responses (CSP, X-Frame-Options, nosniff, etc.) - WS broadcasts scoped to server members (prevents cross-server data leak) - Webhook tokens stored as SHA-256 hashes (not plaintext) P2 fixes: - CSRF protection via Origin header validation on state-changing requests - MANAGE_CHANNELS permission enforced on channel update/delete - Upload validation: 25MB limit, extension allowlist, server-side MIME check - File serve: path traversal protection + Content-Disposition: attachment Files: 23 changed, +499/-100. Builds clean (go build, go vet, npm build). Zero CVEs (govulncheck, npm audit).
This commit is contained in:
@@ -138,15 +138,16 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
token := genToken()
|
||||
tokenHash := hashToken(token)
|
||||
|
||||
var wh webhookWithToken
|
||||
var avatar sql.NullString
|
||||
var createdAt sql.NullString
|
||||
err = h.db.QueryRowContext(r.Context(), `
|
||||
INSERT INTO webhooks (channel_id, name, token, avatar, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
INSERT INTO webhooks (channel_id, name, token, token_hash, avatar, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, channel_id, name, avatar, created_by, created_at::text
|
||||
`, channelID, req.Name, token, req.Avatar, userID).Scan(
|
||||
`, channelID, req.Name, token, tokenHash, req.Avatar, userID).Scan(
|
||||
&wh.ID, &wh.ChannelID, &wh.Name, &avatar, &wh.CreatedBy, &createdAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -300,12 +301,12 @@ func (h *Handler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
webhookID := chi.URLParam(r, "webhookID")
|
||||
token := chi.URLParam(r, "token")
|
||||
|
||||
// Look up webhook and verify token
|
||||
var storedToken, channelID, webhookName string
|
||||
// Look up webhook and verify token via hash comparison
|
||||
var storedHash, channelID, webhookName string
|
||||
var webhookAvatar sql.NullString
|
||||
err := h.db.QueryRowContext(r.Context(), `
|
||||
SELECT token, channel_id, name, avatar FROM webhooks WHERE id = $1
|
||||
`, webhookID).Scan(&storedToken, &channelID, &webhookName, &webhookAvatar)
|
||||
SELECT token_hash, channel_id, name, avatar FROM webhooks WHERE id = $1
|
||||
`, webhookID).Scan(&storedHash, &channelID, &webhookName, &webhookAvatar)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
writeErr(w, http.StatusNotFound, "webhook not found")
|
||||
@@ -315,7 +316,7 @@ func (h *Handler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if token != storedToken {
|
||||
if hashToken(token) != storedHash {
|
||||
writeErr(w, http.StatusUnauthorized, "invalid token")
|
||||
return
|
||||
}
|
||||
@@ -395,12 +396,15 @@ func (h *Handler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
msg.DisplayName = &displayName
|
||||
}
|
||||
|
||||
// Broadcast MESSAGE_CREATE via gateway
|
||||
// Broadcast MESSAGE_CREATE via gateway (scoped to server)
|
||||
if h.hub != nil {
|
||||
h.hub.BroadcastEvent(gateway.Event{
|
||||
Type: gateway.EventMessageCreate,
|
||||
Data: msg,
|
||||
})
|
||||
serverID, _ := h.hub.ServerIDForChannel(r.Context(), channelID)
|
||||
if serverID != "" {
|
||||
h.hub.BroadcastToServer(serverID, gateway.Event{
|
||||
Type: gateway.EventMessageCreate,
|
||||
Data: msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, msg)
|
||||
|
||||
Reference in New Issue
Block a user