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/
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
Policyinterface/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")andRequireRole("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:
CustomRolemodel withpermissions []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 viaRACK_ATTACK_LIMITenv)
2.2 Authentication Rate Limits
| Rule | Limit | Period | Key |
|---|---|---|---|
| super_admin_login/ip | 5 | 5 min | IP |
| super_admin_login/email | 5 | 15 min | |
| 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 | |
| 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_IPSenv (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_ATTACKenv, 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:
- Public assets (
/packs/*,/audio/*) - wildcard origin, GET/OPTIONS only - Public API (
/public/api/*) - wildcard origin, any method - Full API (
*) - only enabled viaCW_API_ONLY_SERVERorENABLE_API_CORSenv, or in development. Exposes auth headers:access-token, client, uid, expiry - 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
AccountUsermembership) - Checks account is active (not suspended)
- For bots: checks bot belongs to account or has inbox in account
- Sets
Current.accountandCurrent.account_userfor 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-256header - Uses
ActiveSupport::SecurityUtils.secure_comparefor 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_tokenandhmac_mandatoryflags - 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: trueand 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
SsrfFiltergem 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_KEYACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEYACTIVE_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_secretencrypted at rest (deterministic for lookup)otp_backup_codesencrypted 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=trueparam - 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-Deliveryheader) - 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) andiat(issued-at) claims - Default expiry: 180 days (configurable via
WIDGET_TOKEN_EXPIRYinstallation config) source_idembedded 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.resetinensureblock 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.identifierandhmac_tokenChannel::WebWidget.website_tokenandhmac_tokenIntegrations::Hook.access_tokenPubsubable.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)
}
23. Cookie Security
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_sslconfigurable viaFORCE_SSLenv (HSTS, secure cookies, redirect HTTP->HTTPS)- Redis SSL verification: defaults to
VERIFY_PEER, configurable toVERIFY_NONEfor 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):
- JWT Authentication - DeviseTokenAuth pattern (access-token, client, uid, expiry headers)
- Rate Limiting - Rack::Attack equivalent with Redis-backed multi-tier rules
- Authorization Policies - Pundit-like deny-by-default with role/permission checks
- SSRF Protection - SafeFetch equivalent for all outbound HTTP
- Data Encryption - ActiveRecord encryption equivalent for secrets/tokens at rest
- MFA/2FA - TOTP + backup codes with timing-safe comparison
- HMAC Verification - Webhook signature verification (inbound and outbound)
- Sensitive Parameter Filtering - Log scrubbing
- Security Headers - CSP, HSTS, X-Frame-Options, X-Content-Type-Options
SHOULD-HAVE (Important):
- CORS - Tiered CORS with conditional full-access
- Bot Access Token Restriction - Endpoint whitelist for bot tokens
- Account Membership Validation - Ensure user belongs to account
- Captcha - hCaptcha for signup
- Audit Logging - Change tracking for all models
- SSO Token - Redis-backed short-lived SSO tokens
- File Upload Security - Content-type validation, size limits, range protection
NICE-TO-HAVE (Enhancement):
- Custom Role Permissions - Enterprise fine-grained permission system
- Reauthorization Tracking - OAuth error counters
- Widget HMAC - API channel HMAC for contact identity verification
- Domain Validation - Request host validation
- Locale Isolation - Per-request locale with validation