mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-10 14:15:42 +00:00
Security fixes: rate-limit widget endpoints, remove JWT from logs, harden middleware checks
Increase max page size to 500 Allow large out of bound page sizes by overriding to maximum set. Reset websocket reconnect attempts when widget is back online
This commit is contained in:
+5
-5
@@ -57,7 +57,7 @@ func handleGetContact(r *fastglue.Request) error {
|
||||
if id <= 0 {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
||||
}
|
||||
c, err := app.user.GetContact(id, "")
|
||||
c, err := app.user.GetContactOrVisitor(id, "")
|
||||
if err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func handleUpdateContact(r *fastglue.Request) error {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
||||
}
|
||||
|
||||
contact, err := app.user.GetContact(id, "")
|
||||
contact, err := app.user.GetContactOrVisitor(id, "")
|
||||
if err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func handleUpdateContact(r *fastglue.Request) error {
|
||||
}
|
||||
|
||||
// Refetch contact and return it
|
||||
contact, err = app.user.GetContact(id, "")
|
||||
contact, err = app.user.GetContactOrVisitor(id, "")
|
||||
if err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
@@ -277,7 +277,7 @@ func handleBlockContact(r *fastglue.Request) error {
|
||||
|
||||
app.lo.Info("setting contact block status", "contact_id", contactID, "enabled", req.Enabled, "actor_id", auser.ID)
|
||||
|
||||
contact, err := app.user.GetContact(contactID, "")
|
||||
contact, err := app.user.GetContactOrVisitor(contactID, "")
|
||||
if err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
@@ -286,7 +286,7 @@ func handleBlockContact(r *fastglue.Request) error {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
contact, err = app.user.GetContact(contactID, "")
|
||||
contact, err = app.user.GetContactOrVisitor(contactID, "")
|
||||
if err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
@@ -144,6 +144,11 @@ func handleSubmitCSATResponse(r *fastglue.Request) error {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid UUID", nil, envelope.InputError)
|
||||
}
|
||||
|
||||
// Trim feedback if it exceeds max length.
|
||||
if len(req.Feedback) > maxCsatFeedbackLength {
|
||||
req.Feedback = req.Feedback[:maxCsatFeedbackLength]
|
||||
}
|
||||
|
||||
// Update CSAT response
|
||||
if err := app.csat.UpdateResponse(uuid, req.Rating, req.Feedback); err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
|
||||
+13
-6
@@ -17,6 +17,8 @@ import (
|
||||
"github.com/zerodha/fastglue"
|
||||
)
|
||||
|
||||
const maxPageSize = 500
|
||||
|
||||
// initHandlers initializes the HTTP routes and handlers for the application.
|
||||
func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
|
||||
// Authentication.
|
||||
@@ -258,11 +260,11 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
|
||||
}))
|
||||
|
||||
// Live chat widget websocket.
|
||||
g.GET("/widget/ws", handleWidgetWS)
|
||||
g.GET("/widget/ws", rateLimit(handleWidgetWS, "widget"))
|
||||
|
||||
// Widget APIs.
|
||||
g.GET("/api/v1/widget/chat/settings/launcher", handleGetChatLauncherSettings)
|
||||
g.GET("/api/v1/widget/chat/settings", handleGetChatSettings)
|
||||
g.GET("/api/v1/widget/chat/settings/launcher", rateLimit(handleGetChatLauncherSettings, "widget"))
|
||||
g.GET("/api/v1/widget/chat/settings", rateLimit(handleGetChatSettings, "widget"))
|
||||
g.POST("/api/v1/widget/chat/conversations/init", rateLimit(widgetAuth(handleChatInit), "widget"))
|
||||
g.GET("/api/v1/widget/chat/conversations", rateLimit(widgetAuth(handleGetConversations), "widget"))
|
||||
g.POST("/api/v1/widget/chat/conversations/{uuid}/update-last-seen", rateLimit(widgetAuth(handleChatUpdateLastSeen), "widget"))
|
||||
@@ -363,7 +365,7 @@ func validateWidgetReferer(app *App, r *fastglue.Request, inboxID int) error {
|
||||
"referer", referer,
|
||||
"inbox_id", inboxID,
|
||||
"trusted_domains", config.TrustedDomains)
|
||||
return r.SendErrorEnvelope(http.StatusForbidden, "Widget not allowed from this origin: "+referer, nil, envelope.PermissionError)
|
||||
return r.SendErrorEnvelope(http.StatusForbidden, "Widget not allowed from this origin", nil, envelope.PermissionError)
|
||||
}
|
||||
app.lo.Debug("widget request from trusted referer allowed", "referer", referer, "inbox_id", inboxID)
|
||||
return nil
|
||||
@@ -373,8 +375,11 @@ func validateWidgetReferer(app *App, r *fastglue.Request, inboxID int) error {
|
||||
func serveWidgetIndexPage(r *fastglue.Request) error {
|
||||
app := r.Context.(*App)
|
||||
|
||||
// Extract inbox ID and validate trusted domains if present
|
||||
// Extract and validate inbox ID.
|
||||
inboxID := r.RequestCtx.QueryArgs().GetUintOrZero("inbox_id")
|
||||
if inboxID <= 0 {
|
||||
return r.SendErrorEnvelope(http.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "inbox_id"), nil, envelope.InputError)
|
||||
}
|
||||
if err := validateWidgetReferer(app, r, inboxID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -488,7 +493,6 @@ func serveWidgetJS(r *fastglue.Request) error {
|
||||
}
|
||||
|
||||
// getPagination extracts page and page_size from query params with defaults.
|
||||
// Defaults: page=1, pageSize=30
|
||||
func getPagination(r *fastglue.Request) (page, pageSize int) {
|
||||
page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
|
||||
pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
|
||||
@@ -498,6 +502,9 @@ func getPagination(r *fastglue.Request) (page, pageSize int) {
|
||||
if pageSize < 1 {
|
||||
pageSize = 30
|
||||
}
|
||||
if pageSize > maxPageSize {
|
||||
pageSize = maxPageSize
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ func widgetAuth(next func(*fastglue.Request) error) func(*fastglue.Request) erro
|
||||
authHeader := string(r.RequestCtx.Request.Header.Peek("Authorization"))
|
||||
|
||||
// For init endpoint, allow requests without JWT (visitor creation)
|
||||
if authHeader == "" && strings.Contains(string(r.RequestCtx.Path()), "/conversations/init") {
|
||||
if authHeader == "" && strings.HasSuffix(string(r.RequestCtx.Path()), "/conversations/init") {
|
||||
return next(r)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func widgetAuth(next func(*fastglue.Request) error) func(*fastglue.Request) erro
|
||||
// Verify JWT using inbox secret
|
||||
claims, err := verifyStandardJWT(jwtToken, inbox.Secret.String)
|
||||
if err != nil {
|
||||
app.lo.Error("invalid JWT", "jwt", jwtToken, "error", err)
|
||||
app.lo.Error("invalid widget JWT", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ export class WidgetWebSocketClient {
|
||||
setupNetworkListeners () {
|
||||
window.addEventListener('online', () => {
|
||||
if (this.socket?.readyState !== WebSocket.OPEN) {
|
||||
this.reconnectAttempts = 0
|
||||
this.reconnectInterval = 1000
|
||||
this.reconnect()
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
conversationsListMaxPageSize = 100
|
||||
conversationsListMaxPageSize = 500
|
||||
)
|
||||
|
||||
// Manager handles the operations related to conversations
|
||||
@@ -1469,7 +1469,7 @@ func (c *Manager) makeConversationsListQuery(viewingUserID, userID int, teamIDs
|
||||
|
||||
// Validate inputs
|
||||
if pageSize > conversationsListMaxPageSize {
|
||||
return "", nil, fmt.Errorf("invalid page size: must be between 1 and %d", conversationsListMaxPageSize)
|
||||
pageSize = conversationsListMaxPageSize
|
||||
}
|
||||
|
||||
if len(listTypes) == 0 {
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxMessagesPerPage = 100
|
||||
maxMessagesPerPage = 500
|
||||
)
|
||||
|
||||
// Run starts a pool of worker goroutines to handle message dispatching via inbox's channel and processes incoming messages. It scans for
|
||||
|
||||
@@ -343,7 +343,7 @@ func (e *Email) processEnvelope(ctx context.Context, client *imapclient.Client,
|
||||
}
|
||||
|
||||
// Check if contact with this email is blocked / disabed, if so, ignore the message.
|
||||
if contact, err := e.userStore.GetContact(0, fromAddress); err != nil {
|
||||
if contact, err := e.userStore.GetContactOrVisitor(0, fromAddress); err != nil {
|
||||
envErr, ok := err.(envelope.Error)
|
||||
if !ok || envErr.ErrorType != envelope.NotFoundError {
|
||||
e.lo.Error("error checking if user is blocked", "email", fromAddress, "error", err)
|
||||
|
||||
@@ -73,7 +73,7 @@ type MessageStore interface {
|
||||
|
||||
// UserStore defines methods for fetching user information.
|
||||
type UserStore interface {
|
||||
GetContact(id int, email string) (umodels.User, error)
|
||||
GetContactOrVisitor(id int, email string) (umodels.User, error)
|
||||
GetAgent(id int, email string) (umodels.User, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -74,14 +74,10 @@ func (u *Manager) UpdateContact(id int, user models.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Manager) GetContact(id int, email string) (models.User, error) {
|
||||
return u.Get(id, email, []string{models.UserTypeContact, models.UserTypeVisitor})
|
||||
}
|
||||
|
||||
// GetAllContacts returns a list of all contacts.
|
||||
func (u *Manager) GetContacts(page, pageSize int, order, orderBy string, filtersJSON string) ([]models.UserCompact, error) {
|
||||
if pageSize > maxListPageSize {
|
||||
return nil, envelope.NewError(envelope.InputError, u.i18n.Ts("globals.messages.pageTooLarge", "max", fmt.Sprintf("%d", maxListPageSize)), nil)
|
||||
pageSize = maxListPageSize
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
|
||||
@@ -34,7 +34,7 @@ var (
|
||||
|
||||
minPassword = 10
|
||||
maxPassword = 72
|
||||
maxListPageSize = 100
|
||||
maxListPageSize = 500
|
||||
|
||||
// ErrPasswordTooLong is returned when the password passed to
|
||||
// GenerateFromPassword is too long (i.e. > 72 bytes).
|
||||
@@ -175,6 +175,11 @@ func (u *Manager) Get(id int, email string, userType []string) (models.User, err
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetContactOrVisitor retrieves a user by ID or email that is either a contact or visitor.
|
||||
func (u *Manager) GetContactOrVisitor(id int, email string) (models.User, error) {
|
||||
return u.Get(id, email, []string{models.UserTypeContact, models.UserTypeVisitor})
|
||||
}
|
||||
|
||||
// GetSystemUser retrieves the system user.
|
||||
func (u *Manager) GetSystemUser() (models.User, error) {
|
||||
return u.Get(0, models.SystemUserEmail, []string{models.UserTypeAgent})
|
||||
|
||||
Reference in New Issue
Block a user