- Validate emails and display names (users, API keys, OAuth signups) against a strict allowlist so stored values can't carry HTML/script payloads - Block notification channel webhooks from targeting private/internal/metadata addresses (SSRF), with an opt-out env var for self-hosted setups that need it - Bound concurrent channel delivery retries so a sustained outage can't spawn unbounded goroutines - Fail closed when the admin check can't reach the database, instead of quietly denying a legitimate admin - Remove envd's dead auth middleware (unused, envd relies on VM isolation not per-request auth) and drop unused username plumbing from the filesystem service
67 lines
1.8 KiB
Go
67 lines
1.8 KiB
Go
package channels
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// WebhookDelivery delivers events to webhook URLs with HMAC signing.
|
|
type WebhookDelivery struct {
|
|
client *http.Client
|
|
}
|
|
|
|
// NewWebhookDelivery constructs a webhook delivery client. When allowPrivate is
|
|
// false the client refuses to connect to non-public addresses (SSRF guard).
|
|
func NewWebhookDelivery(allowPrivate bool) *WebhookDelivery {
|
|
return &WebhookDelivery{
|
|
client: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
CheckRedirect: func(*http.Request, []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
Transport: &http.Transport{
|
|
DialContext: dialContext(allowPrivate),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Deliver signs and POSTs the event payload to the configured URL.
|
|
func (d *WebhookDelivery) Deliver(ctx context.Context, targetURL, secret string, payload []byte) error {
|
|
timestamp := time.Now().UTC().Format(time.RFC3339)
|
|
deliveryID := uuid.New().String()
|
|
|
|
// Compute HMAC-SHA256: sign over "timestamp.body".
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(timestamp + "." + string(payload)))
|
|
signature := "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, strings.NewReader(string(payload)))
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-WRENN-SIGNATURE", signature)
|
|
req.Header.Set("X-Wrenn-Delivery", deliveryID)
|
|
req.Header.Set("X-Wrenn-Timestamp", timestamp)
|
|
|
|
resp, err := d.client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("http post: %w", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("webhook returned %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|