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
@@ -30,6 +30,7 @@ import (
"github.com/gochat/gochat/internal/campaign"
"github.com/gochat/gochat/internal/config"
apiv1 "github.com/gochat/gochat/internal/handler/api/v1"
"github.com/gochat/gochat/internal/middleware"
"github.com/gochat/gochat/internal/model"
channelmodel "github.com/gochat/gochat/internal/model/channel"
"github.com/gochat/gochat/internal/repository"
@@ -243,6 +244,67 @@ func seedWidgetHandlerData(t *testing.T, db *gorm.DB) (*model.Account, *model.In
return account, inbox
}
func TestAllowIframeRequestsScopesEmbeddingToWidgetPage(t *testing.T) {
db, _, handler := setupWidgetHandlerTest(t)
account, _ := seedWidgetHandlerData(t, db)
configJSON, err := json.Marshal(map[string]any{
"website_token": "restricted_widget",
"allowed_domains": "https://allowed.example, https://support.example",
})
require.NoError(t, err)
require.NoError(t, db.Create(&model.Inbox{
AccountID: account.ID, Name: "Restricted Widget", ChannelType: "web_widget", Enabled: true, ChannelConfig: string(configJSON),
}).Error)
invalidConfigJSON, err := json.Marshal(map[string]any{
"website_token": "invalid_widget",
"allowed_domains": "https://allowed.example; frame-ancestors *",
})
require.NoError(t, err)
require.NoError(t, db.Create(&model.Inbox{
AccountID: account.ID, Name: "Invalid Widget", ChannelType: "web_widget", Enabled: true, ChannelConfig: string(invalidConfigJSON),
}).Error)
router := gin.New()
router.Use(middleware.SecurityHeaders(middleware.DefaultSecurityHeadersConfig()))
router.GET("/widget", handler.AllowIframeRequests, func(c *gin.Context) { c.String(http.StatusOK, "widget") })
router.GET("/app", func(c *gin.Context) { c.String(http.StatusOK, "dashboard") })
for _, origin := range []string{"https://go-web-inbox.yqbmb.com", "https://unrelated.example"} {
t.Run("default widget from "+origin, func(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/widget?website_token=handler_ws_token_123", nil)
request.Header.Set("Referer", origin+"/")
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
assert.Equal(t, http.StatusOK, response.Code)
assert.Empty(t, response.Header().Get("X-Frame-Options"))
assert.NotContains(t, response.Header().Get("Content-Security-Policy"), "frame-ancestors")
assert.Contains(t, response.Header().Get("Content-Security-Policy"), "script-src 'self'")
})
}
restricted := httptest.NewRecorder()
router.ServeHTTP(restricted, httptest.NewRequest(http.MethodGet, "/widget?website_token=restricted_widget", nil))
assert.Equal(t, http.StatusOK, restricted.Code)
assert.Empty(t, restricted.Header().Get("X-Frame-Options"))
assert.Contains(t, restricted.Header().Get("Content-Security-Policy"), "frame-ancestors https://allowed.example https://support.example")
assert.Contains(t, restricted.Header().Get("Content-Security-Policy"), "script-src 'self'")
invalid := httptest.NewRecorder()
router.ServeHTTP(invalid, httptest.NewRequest(http.MethodGet, "/widget?website_token=invalid_widget", nil))
assert.Equal(t, http.StatusInternalServerError, invalid.Code)
assert.Equal(t, "DENY", invalid.Header().Get("X-Frame-Options"))
assert.Contains(t, invalid.Header().Get("Content-Security-Policy"), "frame-ancestors 'none'")
dashboard := httptest.NewRecorder()
router.ServeHTTP(dashboard, httptest.NewRequest(http.MethodGet, "/app", nil))
assert.Equal(t, http.StatusOK, dashboard.Code)
assert.Equal(t, "DENY", dashboard.Header().Get("X-Frame-Options"))
assert.Contains(t, dashboard.Header().Get("Content-Security-Policy"), "frame-ancestors 'none'")
}
func seedPublicAPIInbox(t *testing.T, db *gorm.DB) (*model.Account, *model.Inbox, *channelmodel.ChannelAPI) {
t.Helper()