fix(security): SSRF protection for alarm webhooks (FLAW-05)
Discord and custom webhook URLs are checked when saved and before every call: https only, Discord URLs must point to discord.com. A guarded HTTP client resolves hosts itself and connects only to allowed IPs; localhost, link-local and cloud metadata are always blocked, LAN ranges (incl. Tailscale CGNAT) and plain http only with the new admin switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4cdbcbd6b5
commit
62463bc6fd
8 files changed
+288
-16
No files matched your search
+30
-2
@@ -2325,6 +2325,20 @@ func (s *Server) handleUpdateUserSettings(w http.ResponseWriter, r *http.Request
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
// Alarm URLs are checked before they are stored (SSRF protection, see netguard.go)
|
||||
allowPrivate := s.allowPrivateWebhooks()
|
||||
if req.DiscordWebhookURL != nil {
|
||||
if err := validateWebhookURL(*req.DiscordWebhookURL, true, allowPrivate); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Discord-Webhook: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.CustomWebhookURL != nil {
|
||||
if err := validateWebhookURL(*req.CustomWebhookURL, false, allowPrivate); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Webhook-URL: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.Password != nil && strings.TrimSpace(*req.Password) != "" {
|
||||
hash, err := auth.HashPassword(*req.Password)
|
||||
if err != nil {
|
||||
@@ -2933,6 +2947,12 @@ func (s *Server) handleAdminUpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.DiscordWebhookURL != nil {
|
||||
if err := validateWebhookURL(*req.DiscordWebhookURL, true, s.allowPrivateWebhooks()); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Discord-Webhook: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
u, err := s.store.UpdateUser(targetUserID, req.DisplayName, req.Email, req.Role, req.IsActive, req.DiscordWebhookURL, nil)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
@@ -3112,7 +3132,11 @@ func (s *Server) sendEmailNotification(toEmail, subject, bodyText string) error
|
||||
}
|
||||
|
||||
func (s *Server) sendDiscordNotification(webhookURL, title, description, dueDate, priority string) error {
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
allowPrivate := s.allowPrivateWebhooks()
|
||||
if err := validateWebhookURL(webhookURL, true, allowPrivate); err != nil {
|
||||
return err
|
||||
}
|
||||
httpClient := guardedClient(allowPrivate)
|
||||
payload := map[string]interface{}{
|
||||
"content": "⏰ **MindSift Aufgaben-Alarm**",
|
||||
"embeds": []map[string]interface{}{
|
||||
@@ -3142,7 +3166,11 @@ func (s *Server) sendDiscordNotification(webhookURL, title, description, dueDate
|
||||
}
|
||||
|
||||
func (s *Server) sendCustomWebhookNotification(webhookURL, event, title, notes, dueDate, priority string) error {
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
allowPrivate := s.allowPrivateWebhooks()
|
||||
if err := validateWebhookURL(webhookURL, false, allowPrivate); err != nil {
|
||||
return err
|
||||
}
|
||||
httpClient := guardedClient(allowPrivate)
|
||||
payload := map[string]interface{}{
|
||||
"event": event,
|
||||
"title": title,
|
||||
|
||||
Reference in new issue
Block a user