Files
gochat/frontend/SECURITY_AUDIT_ANALYSIS.md
T
rogee 321c61aaae Vendor Chatwoot Vue 3 frontend into frontend/
Copy the Chatwoot (v4.14.0) frontend runnable subset into frontend/ for
customization:
- app/javascript/ (Vue SPA: dashboard, widget, sdk, portal, superadmin)
- app/views/ (ERB templates for vite-plugin-ruby entrypoint resolution)
- app/helpers/, app/assets/ (Rails view helpers, static assets)
- enterprise/ (Enterprise edition frontend overlay)
- config/vite.json, vite.config.ts, bin/vite (Vite-Rails toolchain)
- package.json, pnpm-lock.yaml, tailwind/postcss/eslint configs
- Gemfile, Gemfile.lock (vite_rails gem for bin/vite binstub)

Excluded Rails backend: controllers, models, services, jobs, mailers,
policies, db, lib, spec, public, node_modules.

Update references to the new frontend location:
- .gitignore: exclude frontend build artifacts (node_modules, tmp, packs),
  keep frontend/bin/ and frontend/vendor/ via negation
- backend/scripts/parity_frontend_smoke.sh: CHATWOOT_DIR default
  reference/chatwoot -> ../frontend
- backend/scripts/parity_frontend_browser_smoke.mjs: same default update
- AGENTS.md: add frontend section with Rails/Vite coupling notes
- README: architecture tree includes frontend/
2026-07-07 14:56:01 +08:00

34 KiB

Chatwoot Security Audit Analysis - Go Implementation Guide

1. Authorization System (Pundit Policies)

1.1 ApplicationPolicy (Base Policy)

File: app/policies/application_policy.rb Security Feature: Base authorization framework using Pundit. Every controller action must pass through a policy check. Default is false (deny-by-default) for all CRUD operations. Only show? checks scope membership. Go Implementation:

  • Create a base Policy interface/struct with deny-by-default methods
  • Use middleware pattern: each handler wrapped with Authorize(policy, action) middleware
  • Pass UserContext{User, Account, AccountUser} through context
type UserContext struct {
    User        *User
    Account     *Account
    AccountUser *AccountUser
}

type Policy interface {
    Index(ctx UserContext) bool
    Show(ctx UserContext, record interface{}) bool
    Create(ctx UserContext) bool
    Update(ctx UserContext) bool
    Destroy(ctx UserContext) bool
}

1.2 Role-Based Access Pattern

Security Feature: Two primary roles: administrator and agent. Administrator has full access, agent has limited access. Some actions are available to all authenticated users. Pattern across policies:

Resource Admin-only Actions Agent-accessible All-user accessible
Account update, subscription, toggle_deletion show, cache_keys, limits update_active_at
Inbox create, update, destroy, campaigns index, show, assignable_agents -
User create, update, destroy, bulk_create index -
Webhook all CRUD - -
Campaign all CRUD - -
Contact import, export, destroy - index, show, create, update, search
Conversation destroy show (inbox/team access) index
Label update, show, create, destroy index -
AutomationRule all CRUD - -
Macro admin for global; author for personal - index, create

Go Implementation:

  • Define RBAC middleware that checks AccountUser.Role (administrator/agent)
  • Per-resource permission matrix in config/DB
  • Use RequireRole("administrator") and RequireRole("administrator", "agent") middleware decorators

1.3 Enterprise Custom Role Permissions

File: enterprise/app/policies/enterprise/conversation_policy.rb, enterprise/app/policies/enterprise/article_policy.rb, enterprise/app/policies/enterprise/report_policy.rb Security Feature: Fine-grained custom role permissions. A user can have a CustomRole with specific named permissions like conversation_manage, conversation_unassigned_manage, conversation_participating_manage, knowledge_base_manage, report_manage. These override the simple admin/agent binary. Go Implementation:

  • CustomRole model with permissions []string
  • Permission strings as constants: PermConversationManage, PermConversationUnassignedManage, etc.
  • Middleware checks: if accountUser.CustomRoleID != 0 { checkCustomPermissions() } else { checkRole() }
func (p ConversationPolicy) Show(ctx UserContext, conv *Conversation) bool {
    // Base check first
    if !baseCheck(ctx, conv) { return false }
    if ctx.AccountUser.CustomRoleID == 0 { return true } // standard roles pass
    perms := ctx.AccountUser.CustomRole.Permissions
    if hasPerm(perms, "conversation_manage") { return true }
    if hasPerm(perms, "conversation_unassigned_manage") && conv.AssigneeID == nil { return true }
    if hasPerm(perms, "conversation_participating_manage") && isParticipant(ctx, conv) { return true }
    return false
}

1.4 Conversation Inbox/Team Access Check

File: app/policies/conversation_policy.rb Security Feature: Even agents can only view conversations in inboxes they're assigned to or teams they belong to. Go Implementation:

func agentCanViewConversation(ctx UserContext, conv *Conversation) bool {
    return inboxAccess(ctx, conv) || teamAccess(ctx, conv)
}
func inboxAccess(ctx UserContext, conv *Conversation) bool {
    // Check if user.inboxes for this account includes conv.InboxID
}
func teamAccess(ctx UserContext, conv *Conversation) bool {
    // Check if user.teams for this account includes conv.TeamID
}

1.5 Inbox Scope Filtering

File: app/policies/inbox_policy.rb (Scope class) Security Feature: When listing inboxes, only return inboxes assigned to the current user. Agents cannot see unassigned inboxes. Go Implementation:

func ListInboxes(ctx UserContext) []*Inbox {
    return ctx.User.AssignedInboxes  // filtered at DB query level
}

1.6 Enterprise Policies Summary

File Feature
custom_role_policy.rb Custom roles CRUD - admin only
agent_capacity_policy_policy.rb Agent capacity config - admin only
sla_policy_policy.rb SLA policies - admin write, agent read
account_saml_settings_policy.rb SAML settings - admin only
company_policy.rb Company CRUD - all access, destroy admin only
enterprise/conversation_policy.rb Custom role-based conversation visibility
enterprise/article_policy.rb Custom role knowledge_base_manage permission
enterprise/report_policy.rb Custom role report_manage permission

2. Rate Limiting (Rack::Attack)

File: config/initializers/rack_attack.rb (280 lines) Security Feature: Comprehensive rate limiting with Redis-backed storage. Covers:

2.1 Global Rate Limit

  • req/ip: 3000 requests/minute per IP (configurable via RACK_ATTACK_LIMIT env)

2.2 Authentication Rate Limits

Rule Limit Period Key
super_admin_login/ip 5 5 min IP
super_admin_login/email 5 15 min email
login/ip 5 5 min IP (excludes MFA requests)
login/email 10 15 min email (excludes MFA)
reset_password/ip 5 30 min IP
reset_password/email 5 1 hour email
resend_confirmation_auth/ip 5 30 min IP

2.3 MFA Rate Limits

Rule Limit Period Key
mfa_verification/ip 5 1 min IP (delete/disable)
mfa_login/ip 10 1 min IP
mfa_login/token 10 1 min MFA token value

2.4 Signup Rate Limit

  • accounts/ip: 5 requests / 30 min per IP

2.5 Widget API Rate Limits (configurable)

  • Widget contact creation, conversation creation per IP and per website token

2.6 Application API Rate Limits

Rule Limit Period Key
conversation transcript 1000 1 hour account_id
attachment upload 60 1 hour account_id
contact search 100 1 min account_id
reports (user level) 100 1 min user_uid:account_id
conversation meta 60 1 min user_uid:account_id

2.7 Safelists

  • Trusted IPs from RACK_ATTACK_ALLOWED_IPS env (always 127.0.0.1, ::1)
  • Health check endpoint /health

2.8 Logging

  • Blocked requests logged with masked token (first 5 chars + [REDACTED]), remote IP, path, user identifier, account ID, method, user agent

2.9 Enable Toggle

  • ENABLE_RACK_ATTACK env, enabled by default in production, disabled in non-production

Go Implementation:

// Use github.com/ulule/limiter or custom middleware with Redis
type RateLimiter struct {
    store RedisStore
}

var rules = []RateLimitRule{
    {Name: "req/ip", Limit: 3000, Period: 60*time.Second, KeyFunc: ByIP},
    {Name: "login/ip", Limit: 5, Period: 5*time.Minute, KeyFunc: ByIP, PathMatch: "/auth/sign_in", Method: "POST", ExcludeParams: []string{"mfa_token"}},
    {Name: "login/email", Limit: 10, Period: 15*time.Minute, KeyFunc: ByEmail, PathMatch: "/auth/sign_in", Method: "POST"},
    {Name: "reset_password/ip", Limit: 5, Period: 30*time.Minute, KeyFunc: ByIP, PathMatch: "/auth/password", Method: "POST"},
    // ... etc
}

func RateLimitMiddleware(limiter *RateLimiter) gin.HandlerFunc {
    return func(c *gin.Context) {
        for _, rule := range rules {
            if rule.Match(c.Request) {
                if !limiter.Allow(rule.Key(c)) {
                    logBlockedRequest(c, rule)
                    c.AbortWithStatus(429)
                    return
                }
            }
        }
        c.Next()
    }
}

3. CORS Configuration

File: config/initializers/cors.rb Security Feature: Rack::Cors middleware with tiered access:

  1. Public assets (/packs/*, /audio/*) - wildcard origin, GET/OPTIONS only
  2. Public API (/public/api/*) - wildcard origin, any method
  3. Full API (*) - only enabled via CW_API_ONLY_SERVER or ENABLE_API_CORS env, or in development. Exposes auth headers: access-token, client, uid, expiry
  4. WebSocket: action_cable.disable_request_forgery_protection = true

Go Implementation:

func CORSMiddleware() gin.HandlerFunc {
    config := cors.Config{
        AllowMethods: []string{"GET", "OPTIONS"},
    }
    // Tier 1: public assets
    // Tier 2: /public/api/* - full CORS
    // Tier 3: /api/* - conditional based on ENABLE_API_CORS env
    // Expose headers: access-token, client, uid, expiry
}

4. Content Security Policy & Permissions Policy

File: config/initializers/content_security_policy.rb, config/initializers/feature_policy.rb, config/initializers/permissions_policy.rb Security Feature: CSP, Feature Policy, and Permissions Policy are defined but currently all commented out (not active). This is a gap - CSP should be enabled for XSS protection. Go Implementation:

// Add security headers middleware
func SecurityHeadersMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'")
        c.Header("X-Content-Type-Options", "nosniff")
        c.Header("X-Frame-Options", "DENY")
        c.Header("X-XSS-Protection", "1; mode=block")
        c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
        c.Header("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
        c.Next()
    }
}

Recommendation: gochat MUST implement CSP actively - Chatwoot has it disabled which is a security gap.


5. JWT / Token Authentication

5.1 DeviseTokenAuth Configuration

File: config/initializers/devise_token_auth.rb Security Feature:

  • Token-based auth via headers: access-token, client, uid, expiry, token-type
  • change_headers_on_each_request = false (tokens don't rotate per request)
  • Token lifespan: 2 months
  • remove_tokens_after_password_reset = true (invalidate all sessions on password change)
  • Max 25 concurrent devices per user
  • Auth headers exposed in CORS

5.2 Auth Helper

File: app/controllers/concerns/auth_helper.rb Security Feature: Returns auth token headers in responses for authenticated users. Go Implementation:

func SendAuthHeaders(c *gin.Context, user *User) {
    token := user.CreateNewAuthToken()
    c.Header("access-token", token.AccessToken)
    c.Header("token-type", "Bearer")
    c.Header("client", token.Client)
    c.Header("expiry", token.Expiry)
    c.Header("uid", user.UID)
}

5.3 Access Token Auth (Bot/API Token)

File: app/controllers/concerns/access_token_auth_helper.rb Security Feature: Alternative auth via api_access_token header for AgentBots and Users with access tokens. Bots are restricted to specific controller actions. Go Implementation:

func AuthenticateAccessToken(c *gin.Context) {
    token := c.GetHeader("api_access_token")
    if token == "" { return }
    accessToken := FindAccessTokenByToken(token)
    if accessToken == nil { abortUnauthorized(c, "Invalid Access Token"); return }
    c.Set("currentUser", accessToken.Owner)
}
// Bot restriction: only specific endpoints allowed
var BotAccessibleEndpoints = map[string][]string{
    "conversations": {"toggle_status", "create", "update"},
    "messages": {"create"},
}

6. Account Access Authorization

File: app/controllers/concerns/ensure_current_account_helper.rb Security Feature:

  • Validates user belongs to the account (via AccountUser membership)
  • Checks account is active (not suspended)
  • For bots: checks bot belongs to account or has inbox in account
  • Sets Current.account and Current.account_user for downstream use

Go Implementation:

func EnsureCurrentAccount(c *gin.Context) {
    accountID := c.Param("account_id")
    account := FindAccount(accountID)
    if !account.Active { abortUnauthorized(c, "Account is suspended"); return }
    user := c.MustGet("currentUser").(*User)
    accountUser := account.FindAccountUser(user.ID)
    if accountUser == nil { abortUnauthorized(c, "Not authorized for this account"); return }
    c.Set("currentAccount", account)
    c.Set("currentAccountUser", accountUser)
}

7. HMAC / Webhook Signature Verification

7.1 Meta (Facebook/Instagram/WhatsApp) Webhook Verification

File: app/controllers/concerns/meta_token_verify_concern.rb Security Feature:

  • HMAC-SHA256 signature verification on webhook payloads using X-Hub-Signature-256 header
  • Uses ActiveSupport::SecurityUtils.secure_compare for timing-attack-safe comparison
  • Verifies against multiple app secrets (supports multiple channel configs)
  • Also handles webhook subscription verification via hub.verify_token

Go Implementation:

func VerifyMetaSignature(c *gin.Context, secrets []string) bool {
    signature := c.GetHeader("X-Hub-Signature-256")
    if !strings.HasPrefix(signature, "sha256=") { return false }
    body := getRawBody(c)
    for _, secret := range secrets {
        if secret == "" { continue }
        expected := "sha256=" + hmacSHA256(secret, body)
        // Use crypto/subtle.ConstantTimeCompare for timing-safe comparison
        if subtle.ConstantTimeCompare([]byte(expected), []byte(signature)) == 1 {
            return true
        }
    }
    return false
}

7.2 API Channel HMAC

Files: app/models/channel/api.rb, app/controllers/public/api/v1/inboxes/contacts_controller.rb Security Feature:

  • API channels have hmac_token and hmac_mandatory flags
  • Contact identity verification: identifier_hash == HMAC-SHA256(hmac_token, identifier)
  • When hmac_mandatory=true, requests without valid HMAC are rejected
  • Verified contacts get hmac_verified: true and can access more conversation data

Go Implementation:

func VerifyHMAC(c *gin.Context, hmacToken string, identifier string, identifierHash string) bool {
    expected := hmacSHA256(hmacToken, identifier)
    return subtle.ConstantTimeCompare([]byte(expected), []byte(identifierHash)) == 1
}

7.3 Webhook Secretable

File: app/models/concerns/webhook_secretable.rb Security Feature: Auto-generates secret token for webhook models, encrypts it at rest if encryption is configured.


8. SSRF Protection (SafeFetch)

Files: lib/safe_fetch.rb, lib/safe_fetch/fetcher.rb, lib/safe_fetch/request_options.rb Security Feature: Comprehensive SSRF protection for outbound HTTP requests:

  • Uses SsrfFilter gem which blocks requests to private/internal IPs (10.x, 172.16-31.x, 192.168.x, 127.x, ::1, etc.)
  • URL validation: scheme check, URI parsing
  • Content type validation: only allowed content types (image/, video/) by default, configurable
  • File size limits: max bytes with streaming abort if exceeded (default 40MB fallback)
  • Timeout controls: open_timeout=2s, read_timeout=20s
  • Strips sensitive headers (authorization, cookie, proxy-authorization) from cross-origin requests
  • All webhook calls use SafeFetch

Go Implementation:

type SafeFetch struct {
    MaxBytes           int64
    OpenTimeout        time.Duration  // 2s
    ReadTimeout        time.Duration  // 20s
    AllowedContentTypes []string
    StripSensitiveHeaders []string  // authorization, cookie, proxy-authorization
}

func (sf *SafeFetch) Fetch(url string, opts RequestOptions) (*Result, error) {
    // 1. Parse and validate URL scheme (http/https only)
    // 2. Resolve hostname and check against private IP ranges
    //    - Block: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, ::1, fd00::/8
    // 3. Strip sensitive headers for cross-origin requests
    // 4. Set timeouts
    // 5. Stream response, abort if content-type not in allowed list
    // 6. Abort if bytes exceed MaxBytes limit
    // 7. Return sanitized result
}

// Use for all outbound HTTP: webhook calls, avatar fetches, file downloads

9. Data Encryption

9.1 ActiveRecord Encryption

File: config/application.rb (lines 73-85) Security Feature: Rails ActiveRecord encryption with env-configured keys:

  • ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
  • ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
  • ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
  • Supports unencrypted data (backward compat), extended queries, key references for rotation

9.2 Encrypted Fields

Security Feature: Sensitive fields encrypted at rest:

Model Encrypted Fields
User otp_secret (deterministic), otp_backup_codes
Webhook secret
Channel::Line line_channel_secret, line_channel_token
Channel::Instagram access_token
Channel::Email imap_password, smtp_password
Channel::FacebookPage page_access_token, user_access_token
Channel::TwilioSms auth_token
Channel::TwitterProfile twitter_access_token, twitter_access_token_secret
Channel::TikTok access_token, refresh_token
Channel::Telegram bot_token (deterministic)
Channel::WhatsApp provider_config (JSON containing API keys)

Go Implementation:

// Use GORM encrypt/decrypt hooks or field-level encryption
// For deterministic encryption (searchable): AES-SIV
// For non-deterministic: AES-256-GCM with random nonce
type EncryptedField struct {}

func (e EncryptedField) Encrypt(value string, key []byte) string {
    // AES-256-GCM encryption
}
func (e EncryptedField) Decrypt(value string, key []byte) string {
    // AES-256-GCM decryption
}
func (e EncryptedField) EncryptDeterministic(value string, key []byte) string {
    // AES-SIV for searchable fields
}

10. MFA/2FA

Files: app/services/mfa/token_service.rb, app/services/mfa/authentication_service.rb, app/services/mfa/management_service.rb Security Feature:

  • TOTP-based MFA (time-based OTP via otp_secret)
  • Backup codes (10 single-use codes, constant-time comparison, one-time use with marking as used)
  • MFA token for login verification: JWT with user_id + short expiry
  • Separate rate limits for MFA verification attempts
  • otp_secret encrypted at rest (deterministic for lookup)
  • otp_backup_codes encrypted at rest

Go Implementation:

// Use github.com/pquerna/otp for TOTP
func EnableMFA(user *User) {
    secret := GenerateOTPSecret()
    user.OTPSecret = EncryptDeterministic(secret)  // searchable
    user.Save()
}
func VerifyOTP(user *User, code string) bool {
    secret := DecryptDeterministic(user.OTPSecret)
    return totp.Validate(secret, code)
}
func VerifyBackupCode(user *User, code string) bool {
    codes := Decrypt(user.OTPBackupCodes)
    for i, stored := range codes {
        if subtle.ConstantTimeCompare([]byte(stored), []byte(code)) == 1 && stored != "XXXXXXXX" {
            codes[i] = "XXXXXXXX"  // mark used
            user.OTPBackupCodes = Encrypt(codes)
            user.Save()
            return true
        }
    }
    return false
}

11. SSO Authentication

File: app/models/concerns/sso_authenticatable.rb Security Feature:

  • SSO auth tokens: random 64-char hex, stored in Redis with 5-minute TTL
  • SSO link generation with encoded email
  • Impersonation support via impersonation=true param
  • Token validation and invalidation via Redis

Go Implementation:

func GenerateSSOToken(user *User) string {
    token := randomHex(32)
    redis.SetEX(fmt.Sprintf("sso_token:%d:%s", user.ID, token), "1", 5*time.Minute)
    return token
}
func ValidateSSOToken(user *User, token string) bool {
    return redis.Exists(fmt.Sprintf("sso_token:%d:%s", user.ID, token))
}
func InvalidateSSOToken(user *User, token string) {
    redis.Del(fmt.Sprintf("sso_token:%d:%s", user.ID, token))
}

12. Sensitive Parameter Filtering

File: config/initializers/filter_parameter_logging.rb Security Feature: Logs are scrubbed of sensitive parameters:

  • Explicit list: password, secret, _key, auth, crypt, salt, certificate, otp, access, private, protected, ssn, otp_secret, otp_code, backup_code, mfa_token, otp_backup_codes
  • Regex: matches any key containing "token" EXCEPT "website_token" Go Implementation:
var sensitiveParams = []string{"password", "secret", "key", "auth", "crypt", "salt", 
    "certificate", "otp", "access", "private", "protected", "ssn", 
    "otp_secret", "otp_code", "backup_code", "mfa_token", "otp_backup_codes"}
var tokenRegex = regexp.MustCompile(`(?i)\btoken\b`)
var websiteTokenRegex = regexp.MustCompile(`\bwebsite_token\b`)

func FilterLogParams(params map[string]interface{}) map[string]interface{} {
    filtered := make(map[string]interface{})
    for k, v := range params {
        if isSensitive(k) {
            filtered[k] = "[FILTERED]"
        } else {
            filtered[k] = v
        }
    }
    return filtered
}
func isSensitive(key string) bool {
    for _, s := range sensitiveParams {
        if strings.Contains(strings.ToLower(key), s) { return true }
    }
    if tokenRegex.MatchString(key) && !websiteTokenRegex.MatchString(key) { return true }
    return false
}

13. Captcha Verification

File: lib/chatwoot_captcha.rb Security Feature: hCaptcha integration for signup protection. Server-side verification via hCaptcha API. If HCAPTCHA_SERVER_KEY is not configured, captcha check is skipped (returns true). Go Implementation:

func ValidateCaptcha(clientResponse string) bool {
    serverKey := config.HCaptchaServerKey
    if serverKey == "" { return true }  // skip if not configured
    if clientResponse == "" { return false }
    resp, err := http.PostForm("https://hcaptcha.com/siteverify", url.Values{
        "response": {clientResponse},
        "secret":   {serverKey},
    })
    if err != nil { return false }
    return resp.JSON()["success"] == true
}

14. Webhook Signature (Outbound)

File: lib/webhooks/trigger.rb Security Feature:

  • Outbound webhooks include HMAC-SHA256 signature header (X-Chatwoot-Signature-256)
  • Signature computed from secret + payload body
  • Delivery ID tracking (X-Chatwoot-Delivery header)
  • Uses SafeFetch for SSRF-safe outbound HTTP
  • Retry logic for agent bots (429, 500 status codes)

Go Implementation:

func SendWebhook(url string, payload interface{}, secret string, deliveryID string) error {
    body := json.Marshal(payload)
    signature := hmacSHA256(secret, body)
    headers := map[string]string{
        "Content-Type":           "application/json",
        "X-Chatwoot-Signature-256": "sha256=" + signature,
        "X-Chatwoot-Delivery":    deliveryID,
    }
    return SafeFetch(url, headers, body)  // SSRF-safe request
}

15. Widget Token (JWT)

File: app/services/widget/token_service.rb, app/services/base_token_service.rb Security Feature:

  • JWT tokens for widget client auth
  • HS256 algorithm, signed with secret_key_base
  • Includes exp (expiry) and iat (issued-at) claims
  • Default expiry: 180 days (configurable via WIDGET_TOKEN_EXPIRY installation config)
  • source_id embedded in token for contact identification

Go Implementation:

func GenerateWidgetToken(sourceID string, secretKey string) string {
    expiryDays := GetWidgetTokenExpiry()  // default 180
    claims := jwt.MapClaims{
        "source_id": sourceID,
        "exp":       time.Now().Add(time.Duration(expiryDays) * 24 * time.Hour).Unix(),
        "iat":       time.Now().Unix(),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(secretKey))
}

16. Domain Validation

File: app/controllers/concerns/domain_helper.rb Security Feature: Validates that request host matches FRONTEND_URL or HELPCENTER_URL domains to prevent CSRF/domain confusion attacks. Go Implementation:

func IsChatwootDomain(host string) bool {
    frontendHost := parseURL(config.FrontendURL).Host
    helpcenterHost := parseURL(config.HelpcenterURL).Host
    return host == frontendHost || host == helpcenterHost
}

17. Locale Security

File: app/controllers/concerns/switch_locale.rb Security Feature: Validates and sanitizes locale parameter before use. Uses I18n.with_locale to prevent locale bleeding across requests (per-request isolation). Go Implementation:

func ValidateLocale(locale string) string {
    supported := []string{"en", "zh", "ja", ...}
    for _, s := range supported {
        if locale == s { return s }
    }
    return "en"  // default fallback
}
// Always set locale per-request in context, never globally

18. Request Exception Handling (Thread Safety)

File: app/controllers/concerns/request_exception_handler.rb Security Feature:

  • Centralized error handling with proper HTTP status codes
  • Current.reset in ensure block to prevent thread variable leaks (critical in multi-threaded servers like Puma)
  • Pundit NotAuthorizedError caught and returned as 401
  • ActiveRecord::RecordNotFound returned as 404 (no details leaked)

Go Implementation:

func ErrorHandlerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            // In Go, context is per-request naturally, no thread leak concern
            // But ensure request-scoped variables are cleaned up
        }()
        c.Next()
        // Handle known error types
        if errors.Is(c.Errors.Last(), ErrNotFound) {
            c.JSON(404, gin.H{"error": "Resource could not be found"})
        } else if errors.Is(c.Errors.Last(), ErrNotAuthorized) {
            c.JSON(401, gin.H{"error": "You are not authorized to do this action"})
        }
    }
}

19. File Upload Security

File: config/initializers/active_storage.rb Security Feature:

  • Content type validation for inline serving (explicit audio MIME whitelist)
  • Direct upload metadata filter: strips internal metadata keys (identified, analyzed, composed)
  • Range request protection: max 1 range per request, max 100MB chunk size (prevents range bomb attacks)

Go Implementation:

var AllowedInlineContentTypes = []string{"image/jpeg", "image/png", "image/gif", 
    "application/pdf", "audio/webm", "audio/ogg", "audio/mpeg", "audio/mp4"}

func ValidateUpload(contentType string, fileSize int64) error {
    if !isAllowedContentType(contentType) { return ErrInvalidContentType }
    if fileSize > MaxUploadSize { return ErrFileTooLarge }
    return nil
}

// Range request protection
func ServeFileRange(c *gin.Context, blob *Blob, rangeHeader string) {
    ranges := parseByteRanges(rangeHeader, blob.Size)
    if len(ranges) > 1 || ranges[0].Size() > 100*1024*1024 {
        c.Status(416)  // Range Not Satisfiable
        return
    }
    // serve range
}

20. Attachment Concern

File: app/controllers/concerns/attachment_concern.rb Security Feature: Validates that attachment blobs exist and belong to the record (prevents arbitrary blob injection in automation actions). Go Implementation:

func ValidateAttachments(actions []Action, record interface{}) ([]Blob, []Action, error) {
    var blobs []Blob
    for _, action := range actions {
        if action.Name == "send_attachment" {
            blobID := action.Params[0]
            blob := FindSignedBlob(blobID)
            if blob == nil { return nil, nil, ErrInvalidAttachment }
            // Verify blob belongs to record
            if !blobBelongsToRecord(record, blob.ID) { return nil, nil, ErrInvalidAttachment }
            blobs = append(blobs, blob)
        }
    }
    return blobs, actions, nil
}

21. Reauthorization Tracking

File: app/models/concerns/reauthorizable.rb Security Feature: Tracks OAuth authorization errors with Redis counters. After threshold (2 errors), marks channel as needing reauthorization and sends alert emails. Prevents silent OAuth failures. Go Implementation:

func TrackAuthError(channel *Channel) {
    key := fmt.Sprintf("auth_error_count:%d", channel.ID)
    count := redis.Incr(key)
    if count >= AuthErrorThreshold {
        channel.ReauthorizationRequired = true
        channel.Save()
        SendReauthEmail(channel)
    }
}

22. Secure Token Generation

Files: Multiple models using has_secure_token Security Feature: Rails has_secure_token generates cryptographically secure random tokens for:

  • AccessToken.token (for API bot auth)
  • Webhook.secret (for webhook HMAC signing)
  • Channel::Api.identifier and hmac_token
  • Channel::WebWidget.website_token and hmac_token
  • Integrations::Hook.access_token
  • Pubsubable.pubsub_token

Go Implementation:

func GenerateSecureToken(length int) string {
    b := make([]byte, length)
    _, err := rand.Read(b)
    if err != nil { panic(err) }
    return hex.EncodeToString(b)
}
// Or use crypto/rand for URL-safe tokens:
func GenerateSecureTokenURLSafe(length int) string {
    b := make([]byte, length)
    rand.Read(b)
    return base64.URLEncoding.EncodeToString(b)
}

File: config/initializers/cookies_serializer.rb Security Feature: Uses JSON serializer for cookies (prevents Marshal deserialization attacks). Also, Devise config references secure cookie option. Go Implementation:

// Use JSON-only cookie serialization
// Never use gob or binary serialization for cookies
// Set Secure: true, SameSite: Lax for all auth cookies

24. SSL/TLS Configuration

Files: config/environments/production.rb, config/environments/staging.rb Security Feature:

  • force_ssl configurable via FORCE_SSL env (HSTS, secure cookies, redirect HTTP->HTTPS)
  • Redis SSL verification: defaults to VERIFY_PEER, configurable to VERIFY_NONE for Heroku compatibility

Go Implementation:

// In Gin/Go, use TLS middleware:
func ForceSSLMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.Request.Header.Get("X-Forwarded-Proto") != "https" && config.ForceSSL {
            httpsURL := "https://" + c.Request.Host + c.Request.URL.Path
            c.Redirect(301, httpsURL)
            c.Abort()
            return
        }
        c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        c.Next()
    }
}

25. Audit Logging

File: config/initializers/audited.rb Security Feature: Uses audited gem with enterprise AuditLog class for tracking all model changes. Provides accountability trail. Go Implementation:

// GORM hook-based audit logging
func AuditHook(db *gorm.DB) {
    if db.Statement.Schema != nil {
        audit := AuditLog{
            ActionType:  getActionType(db),
            ModelName:   db.Statement.Schema.Table,
            ModelID:     db.Statement.Reflect.Value.FieldByName("ID"),
            Changes:     getChanges(db),
            UserID:      getCurrentUserID(db.Statement.Context),
            AccountID:   getCurrentAccountID(db.Statement.Context),
            CreatedAt:   time.Now(),
        }
        db.Session(&gorm.Session{NewDB: true}).Create(&audit)
    }
}

26. Super Admin Authentication

File: app/controllers/super_admin/application_controller.rb Security Feature: Separate Devise authentication namespace for super admin. Separate rate limiting. Super admin cannot access regular user sessions. Go Implementation:

// Separate auth middleware for super admin routes
func AuthenticateSuperAdmin() gin.HandlerFunc {
    return func(c *gin.Context) {
        session := getSession(c)
        if session.SuperAdminID == 0 {
            c.Redirect(302, "/super_admin/sign_in")
            c.Abort()
        }
        c.Next()
    }
}
// Super admin routes on separate path group with separate rate limits

27. VAPID (Web Push) Key Security

File: lib/vapid_service.rb Security Feature: VAPID keys for web push notification authentication. Auto-generated if not configured, stored as installation config (encrypted in DB). Go Implementation:

func GetVAPIDKeys() (publicKey string, privateKey string) {
    // Generate ECDSA P-256 keys if not configured
    // Store in DB config, load from env as fallback
}

Summary: Security Feature Priority for GoChat Implementation

MUST-HAVE (Critical):

  1. JWT Authentication - DeviseTokenAuth pattern (access-token, client, uid, expiry headers)
  2. Rate Limiting - Rack::Attack equivalent with Redis-backed multi-tier rules
  3. Authorization Policies - Pundit-like deny-by-default with role/permission checks
  4. SSRF Protection - SafeFetch equivalent for all outbound HTTP
  5. Data Encryption - ActiveRecord encryption equivalent for secrets/tokens at rest
  6. MFA/2FA - TOTP + backup codes with timing-safe comparison
  7. HMAC Verification - Webhook signature verification (inbound and outbound)
  8. Sensitive Parameter Filtering - Log scrubbing
  9. Security Headers - CSP, HSTS, X-Frame-Options, X-Content-Type-Options

SHOULD-HAVE (Important):

  1. CORS - Tiered CORS with conditional full-access
  2. Bot Access Token Restriction - Endpoint whitelist for bot tokens
  3. Account Membership Validation - Ensure user belongs to account
  4. Captcha - hCaptcha for signup
  5. Audit Logging - Change tracking for all models
  6. SSO Token - Redis-backed short-lived SSO tokens
  7. File Upload Security - Content-type validation, size limits, range protection

NICE-TO-HAVE (Enhancement):

  1. Custom Role Permissions - Enterprise fine-grained permission system
  2. Reauthorization Tracking - OAuth error counters
  3. Widget HMAC - API channel HMAC for contact identity verification
  4. Domain Validation - Request host validation
  5. Locale Isolation - Per-request locale with validation