HH-540: scope iframe policy to Widget page (#123)

* fix(HH-540): scope iframe policy to widget page

* fix(HH-540): reject non-string allowed domains

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-23 18:37:08 +08:00
committed by GitHub
co-authored by rogee
parent 867092fd99
commit a2e4f9a1e8
9 changed files with 175 additions and 2 deletions
@@ -29,6 +29,70 @@ func NewHandler(widgetService *service.WidgetService) *WidgetHandler {
}
}
// AllowIframeRequests applies Chatwoot's WidgetsController iframe policy only
// to the widget HTML response.
func (h *WidgetHandler) AllowIframeRequests(c *gin.Context) {
inbox, err := h.widgetService.GetInboxByWebsiteToken(c.Request.Context(), strings.TrimSpace(c.Query("website_token")))
if err != nil {
c.Status(http.StatusNotFound)
c.Abort()
return
}
config, err := service.ParseWebWidgetConfig(inbox.ChannelConfig)
if err != nil {
c.Status(http.StatusInternalServerError)
c.Abort()
return
}
frameAncestors, ok := widgetFrameAncestors(config.AllowedDomains)
if !ok {
c.Status(http.StatusInternalServerError)
c.Abort()
return
}
c.Writer.Header().Del("X-Frame-Options")
c.Header("Content-Security-Policy", widgetContentSecurityPolicy(c.Writer.Header().Get("Content-Security-Policy"), frameAncestors))
}
func widgetFrameAncestors(allowedDomains string) (string, bool) {
domains := make([]string, 0)
for _, domain := range strings.Split(allowedDomains, ",") {
domain = strings.TrimSpace(domain)
if domain == "" {
continue
}
if strings.ContainsAny(domain, "; \t\r\n") {
return "", false
}
domains = append(domains, domain)
}
return strings.Join(domains, " "), true
}
func widgetContentSecurityPolicy(policy, frameAncestors string) string {
directives := strings.Split(policy, ";")
result := make([]string, 0, len(directives))
found := false
for _, directive := range directives {
directive = strings.TrimSpace(directive)
if strings.HasPrefix(directive, "frame-ancestors ") {
found = true
if frameAncestors != "" {
result = append(result, "frame-ancestors "+frameAncestors)
}
continue
}
if directive != "" {
result = append(result, directive)
}
}
if frameAncestors != "" && !found {
result = append(result, "frame-ancestors "+frameAncestors)
}
return strings.Join(result, "; ")
}
// Init handles widget initialization — authenticates/creates a contact
// and returns a widget_token (pubsub_token) for subsequent requests.
// POST /widget/init