Moderate severityNVD Advisory· Published Mar 20, 2025· Updated Apr 4, 2025
Cross-Site Scripting (XSS) in mudler/localai
CVE-2024-9900
Description
mudler/localai version v2.21.1 contains a Cross-Site Scripting (XSS) vulnerability in its search functionality. The vulnerability arises due to improper sanitization of user input, allowing the injection and execution of arbitrary JavaScript code. This can lead to the execution of malicious scripts in the context of the victim's browser, potentially compromising user sessions, stealing session cookies, redirecting users to malicious websites, or manipulating the DOM.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/mudler/LocalAIGo | < 2.22.0 | 2.22.0 |
Affected products
1- Range: unspecified
Patches
1a1634b219a4efix: roll out bluemonday Sanitize more widely (#3794)
6 files changed · +37 −29
core/http/elements/gallery.go+10 −13 modified@@ -6,6 +6,7 @@ import ( "github.com/chasefleming/elem-go" "github.com/chasefleming/elem-go/attrs" + "github.com/microcosm-cc/bluemonday" "github.com/mudler/LocalAI/core/gallery" "github.com/mudler/LocalAI/core/p2p" "github.com/mudler/LocalAI/core/services" @@ -41,7 +42,7 @@ func DoneProgress(galleryID, text string, showDelete bool) string { "tabindex": "-1", "autofocus": "", }, - elem.Text(text), + elem.Text(bluemonday.StrictPolicy().Sanitize(text)), ), elem.If(showDelete, deleteButton(galleryID, modelName), reInstallButton(galleryID)), ).Render() @@ -57,7 +58,7 @@ func ErrorProgress(err, galleryName string) string { "tabindex": "-1", "autofocus": "", }, - elem.Text("Error "+err), + elem.Text("Error "+bluemonday.StrictPolicy().Sanitize(err)), ), installButton(galleryName), ).Render() @@ -170,7 +171,7 @@ func P2PNodeBoxes(nodes []p2p.NodeData) string { attrs.Props{ "class": "text-gray-200 font-semibold ml-2 mr-1", }, - elem.Text(n.ID), + elem.Text(bluemonday.StrictPolicy().Sanitize(n.ID)), ), elem.Text("Status: "), elem.If( @@ -227,7 +228,7 @@ func StartProgressBar(uid, progress, text string) string { "tabindex": "-1", "autofocus": "", }, - elem.Text(text), + elem.Text(bluemonday.StrictPolicy().Sanitize(text)), //Perhaps overly defensive elem.Div(attrs.Props{ "hx-get": "/browse/job/progress/" + uid, "hx-trigger": "every 600ms", @@ -249,9 +250,7 @@ func cardSpan(text, icon string) elem.Node { "class": icon + " pr-2", }), - elem.Text(text), - - //elem.Text(text), + elem.Text(bluemonday.StrictPolicy().Sanitize(text)), ) } @@ -285,11 +284,9 @@ func searchableElement(text, icon string) elem.Node { elem.I(attrs.Props{ "class": icon + " pr-2", }), - elem.Text(text), + elem.Text(bluemonday.StrictPolicy().Sanitize(text)), ), ), - - //elem.Text(text), ) } @@ -303,7 +300,7 @@ func link(text, url string) elem.Node { elem.I(attrs.Props{ "class": "fas fa-link pr-2", }), - elem.Text(text), + elem.Text(bluemonday.StrictPolicy().Sanitize(text)), ) } func installButton(galleryName string) elem.Node { @@ -387,13 +384,13 @@ func ListModels(models []*gallery.GalleryModel, processTracker ProcessTracker, g attrs.Props{ "class": "mb-2 text-xl font-bold leading-tight", }, - elem.Text(m.Name), + elem.Text(bluemonday.StrictPolicy().Sanitize(m.Name)), ), elem.P( attrs.Props{ "class": "mb-4 text-sm [&:not(:hover)]:truncate text-base", }, - elem.Text(m.Description), + elem.Text(bluemonday.StrictPolicy().Sanitize(m.Description)), ), ) }
core/http/endpoints/openai/assistant.go+9 −8 modified@@ -10,6 +10,7 @@ import ( "time" "github.com/gofiber/fiber/v2" + "github.com/microcosm-cc/bluemonday" "github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/schema" "github.com/mudler/LocalAI/core/services" @@ -83,7 +84,7 @@ func CreateAssistantEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoad if !modelExists(cl, ml, request.Model) { log.Warn().Msgf("Model: %s was not found in list of models.", request.Model) - return c.Status(fiber.StatusBadRequest).SendString("Model " + request.Model + " not found") + return c.Status(fiber.StatusBadRequest).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Model %q not found", request.Model))) } if request.Tools == nil { @@ -147,7 +148,7 @@ func ListAssistantsEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoade // Convert string limit to integer limit, err := strconv.Atoi(limitQuery) if err != nil { - return c.Status(http.StatusBadRequest).SendString(fmt.Sprintf("Invalid limit query value: %s", limitQuery)) + return c.Status(http.StatusBadRequest).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Invalid limit query value: %s", limitQuery))) } // Sort assistants @@ -288,7 +289,7 @@ func GetAssistantEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, } } - return c.Status(fiber.StatusNotFound).SendString(fmt.Sprintf("Unable to find assistant with id: %s", assistantID)) + return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant with id: %s", assistantID))) } } @@ -337,11 +338,11 @@ func CreateAssistantFileEndpoint(cl *config.BackendConfigLoader, ml *model.Model } } - return c.Status(fiber.StatusNotFound).SendString(fmt.Sprintf("Unable to find file_id: %s", request.FileID)) + return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find file_id: %s", request.FileID))) } } - return c.Status(fiber.StatusNotFound).SendString(fmt.Sprintf("Unable to find %q", assistantID)) + return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find %q", assistantID))) } } @@ -442,7 +443,7 @@ func ModifyAssistantEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoad return c.Status(fiber.StatusOK).JSON(newAssistant) } } - return c.Status(fiber.StatusNotFound).SendString(fmt.Sprintf("Unable to find assistant with id: %s", assistantID)) + return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant with id: %s", assistantID))) } } @@ -513,9 +514,9 @@ func GetAssistantFileEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoa if assistantFile.ID == fileId { return c.Status(fiber.StatusOK).JSON(assistantFile) } - return c.Status(fiber.StatusNotFound).SendString(fmt.Sprintf("Unable to find assistant file with file_id: %s", fileId)) + return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant file with file_id: %s", fileId))) } } - return c.Status(fiber.StatusNotFound).SendString(fmt.Sprintf("Unable to find assistant file with assistant_id: %s", assistantID)) + return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant file with assistant_id: %s", assistantID))) } }
core/http/endpoints/openai/files.go+7 −6 modified@@ -8,6 +8,7 @@ import ( "sync/atomic" "time" + "github.com/microcosm-cc/bluemonday" "github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/schema" @@ -49,7 +50,7 @@ func UploadFilesEndpoint(cm *config.BackendConfigLoader, appConfig *config.Appli err = c.SaveFile(file, savePath) if err != nil { - return c.Status(fiber.StatusInternalServerError).SendString("Failed to save file: " + err.Error()) + return c.Status(fiber.StatusInternalServerError).SendString("Failed to save file: " + bluemonday.StrictPolicy().Sanitize(err.Error())) } f := schema.File{ @@ -121,7 +122,7 @@ func GetFilesEndpoint(cm *config.BackendConfigLoader, appConfig *config.Applicat return func(c *fiber.Ctx) error { file, err := getFileFromRequest(c) if err != nil { - return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) + return c.Status(fiber.StatusInternalServerError).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) } return c.JSON(file) @@ -143,14 +144,14 @@ func DeleteFilesEndpoint(cm *config.BackendConfigLoader, appConfig *config.Appli return func(c *fiber.Ctx) error { file, err := getFileFromRequest(c) if err != nil { - return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) + return c.Status(fiber.StatusInternalServerError).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) } err = os.Remove(filepath.Join(appConfig.UploadDir, file.Filename)) if err != nil { // If the file doesn't exist then we should just continue to remove it if !errors.Is(err, os.ErrNotExist) { - return c.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("Unable to delete file: %s, %v", file.Filename, err)) + return c.Status(fiber.StatusInternalServerError).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to delete file: %s, %v", file.Filename, err))) } } @@ -180,12 +181,12 @@ func GetFilesContentsEndpoint(cm *config.BackendConfigLoader, appConfig *config. return func(c *fiber.Ctx) error { file, err := getFileFromRequest(c) if err != nil { - return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) + return c.Status(fiber.StatusInternalServerError).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) } fileContents, err := os.ReadFile(filepath.Join(appConfig.UploadDir, file.Filename)) if err != nil { - return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) + return c.Status(fiber.StatusInternalServerError).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) } return c.Send(fileContents)
core/http/middleware/auth.go+2 −1 modified@@ -7,6 +7,7 @@ import ( "github.com/dave-gray101/v2keyauth" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/keyauth" + "github.com/microcosm-cc/bluemonday" "github.com/mudler/LocalAI/core/config" ) @@ -38,7 +39,7 @@ func getApiKeyErrorHandler(applicationConfig *config.ApplicationConfig) fiber.Er if applicationConfig.OpaqueErrors { return ctx.SendStatus(403) } - return ctx.Status(403).SendString(err.Error()) + return ctx.Status(403).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) } if applicationConfig.OpaqueErrors { return ctx.SendStatus(500)
core/http/routes/ui.go+2 −1 modified@@ -6,6 +6,7 @@ import ( "sort" "strings" + "github.com/microcosm-cc/bluemonday" "github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/gallery" "github.com/mudler/LocalAI/core/http/elements" @@ -171,7 +172,7 @@ func RegisterUIRoutes(app *fiber.App, Search string `form:"search"` }{} if err := c.BodyParser(&form); err != nil { - return c.Status(fiber.StatusBadRequest).SendString(err.Error()) + return c.Status(fiber.StatusBadRequest).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) } models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
.github/ci/modelslist.go+7 −0 modified@@ -6,6 +6,7 @@ import ( "io/ioutil" "os" + "github.com/microcosm-cc/bluemonday" "gopkg.in/yaml.v3" ) @@ -279,6 +280,12 @@ func main() { return } + // Ensure that all arbitrary text content is sanitized before display + for i, m := range models { + models[i].Name = bluemonday.StrictPolicy().Sanitize(m.Name) + models[i].Description = bluemonday.StrictPolicy().Sanitize(m.Description) + } + // render the template data := struct { Models []*GalleryModel
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.