CVE-2024-6095: Server-Side Request Forgery (SSRF) (CWE-918)
value: A vulnerability in the /models/apply endpoint of mudler/localai versions 2.15.0 allows for Server-Side Request Forgery (SSRF) and partial Local File Inclusion (LFI). The endpoint supports both http(s):// and file:// schemes, where the latter can lead to LFI. However, the output is limited due to the length of the error message. This vulnerability can be exploited by an attacker with network access to the LocalAI instance, potentially allowing unauthorized access to internal HTTP(s) servers and partial reading of local files. The issue is fixed in version 2.17.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 5.8
Published: 2024-07-06T18:15Z
Repository: https://github.com/mudler/localai
Language: Go
Files changed: core/http/app_test.go, core/services/gallery.go, embedded/embedded.go, pkg/downloader/uri.go, pkg/downloader/uri_test.go, pkg/gallery/gallery.go, pkg/gallery/models.go, pkg/gallery/request_test.go, pkg/startup/model_preload.go
Commit date: 2024-06-04 14:32:47 +0000
Commit message: fix: `pkg/downloader` should respect basePath for `file://` urls (#2481)

* pass basePath down to pkg/downloader

Signed-off-by: Dave Lee <dave@gray101.com>

* enforce

Signed-off-by: Dave Lee <dave@gray101.com>

---------

Signed-off-by: Dave Lee <dave@gray101.com>
Diff: diff --git a/core/http/app_test.go b/core/http/app_test.go
index 5776b99a..6e9de246 100644
--- a/core/http/app_test.go
+++ b/core/http/app_test.go
@@ -71,11 +71,12 @@ func getModelStatus(url string) (response map[string]interface{}) {
 	}
 	return
 }
 
 func getModels(url string) (response []gallery.GalleryModel) {
-	downloader.GetURI(url, func(url string, i []byte) error {
+	// TODO: No tests currently seem to exercise file:// urls. Fix?
+	downloader.GetURI(url, "", func(url string, i []byte) error {
 		// Unmarshal YAML data into a struct
 		return json.Unmarshal(i, &response)
 	})
 	return
 }
diff --git a/core/services/gallery.go b/core/services/gallery.go
index ed6f6165..e20e733a 100644
--- a/core/services/gallery.go
+++ b/core/services/gallery.go
@@ -30,11 +30,11 @@ func NewGalleryService(modelPath string) *GalleryService {
 	}
 }
 
 func prepareModel(modelPath string, req gallery.GalleryModel, cl *config.BackendConfigLoader, downloadStatus func(string, string, string, float64)) error {
 
-	config, err := gallery.GetGalleryConfigFromURL(req.URL)
+	config, err := gallery.GetGalleryConfigFromURL(req.URL, modelPath)
 	if err != nil {
 		return err
 	}
 
 	config.Files = append(config.Files, req.AdditionalFiles...)
diff --git a/embedded/embedded.go b/embedded/embedded.go
index 438a1352..1fc59b4d 100644
--- a/embedded/embedded.go
+++ b/embedded/embedded.go
@@ -34,14 +34,14 @@ func init() {
 	if err != nil {
 		log.Error().Err(err).Msg("error while unmarshalling embedded modelLibrary")
 	}
 }
 
-func GetRemoteLibraryShorteners(url string) (map[string]string, error) {
+func GetRemoteLibraryShorteners(url string, basePath string) (map[string]string, error) {
 	remoteLibrary := map[string]string{}
 
-	err := downloader.GetURI(url, func(_ string, i []byte) error {
+	err := downloader.GetURI(url, basePath, func(_ string, i []byte) error {
 		return yaml.Unmarshal(i, &remoteLibrary)
 	})
 	if err != nil {
 		return nil, fmt.Errorf("error downloading remote library: %s", err.Error())
 	}
diff --git a/pkg/downloader/uri.go b/pkg/downloader/uri.go
index 797a264b..0848a238 100644
--- a/pkg/downloader/uri.go
+++ b/pkg/downloader/uri.go
@@ -21,20 +21,25 @@ const (
 	HTTPSPrefix       = "https://"
 	GithubURI         = "github:"
 	GithubURI2        = "github://"
 )
 
-func GetURI(url string, f func(url string, i []byte) error) error {
+func GetURI(url string, basePath string, f func(url string, i []byte) error) error {
 	url = ConvertURL(url)
 
 	if strings.HasPrefix(url, "file://") {
 		rawURL := strings.TrimPrefix(url, "file://")
 		// checks if the file is symbolic, and resolve if so - otherwise, this function returns the path unmodified.
 		resolvedFile, err := filepath.EvalSymlinks(rawURL)
 		if err != nil {
 			return err
 		}
+		// Check if the local file is rooted in basePath
+		err = utils.VerifyPath(resolvedFile, basePath)
+		if err != nil {
+			return err
+		}
 		// Read the response body
 		body, err := os.ReadFile(resolvedFile)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/downloader/uri_test.go b/pkg/downloader/uri_test.go
index cd17b7ca..3ab04e56 100644
--- a/pkg/downloader/uri_test.go
+++ b/pkg/downloader/uri_test.go
@@ -8,27 +8,27 @@ import (
 
 var _ = Describe("Gallery API tests", func() {
 	Context("URI", func() {
 		It("parses github with a branch", func() {
 			Expect(
-				GetURI("github:go-skynet/model-gallery/gpt4all-j.yaml", func(url string, i []byte) error {
+				GetURI("github:go-skynet/model-gallery/gpt4all-j.yaml", "", func(url string, i []byte) error {
 					Expect(url).To(Equal("https://raw.githubusercontent.com/go-skynet/model-gallery/main/gpt4all-j.yaml"))
 					return nil
 				}),
 			).ToNot(HaveOccurred())
 		})
 		It("parses github without a branch", func() {
 			Expect(
-				GetURI("github:go-skynet/model-gallery/gpt4all-j.yaml@main", func(url string, i []byte) error {
+				GetURI("github:go-skynet/model-gallery/gpt4all-j.yaml@main", "", func(url string, i []byte) error {
 					Expect(url).To(Equal("https://raw.githubusercontent.com/go-skynet/model-gallery/main/gpt4all-j.yaml"))
 					return nil
 				}),
 			).ToNot(HaveOccurred())
 		})
 		It("parses github with urls", func() {
 			Expect(
-				GetURI("https://raw.githubusercontent.com/go-skynet/model-gallery/main/gpt4all-j.yaml", func(url string, i []byte) error {
+				GetURI("https://raw.githubusercontent.com/go-skynet/model-gallery/main/gpt4all-j.yaml", "", func(url string, i []byte) error {
 					Expect(url).To(Equal("https://raw.githubusercontent.com/go-skynet/model-gallery/main/gpt4all-j.yaml"))
 					return nil
 				}),
 			).ToNot(HaveOccurred())
 		})
diff --git a/pkg/gallery/gallery.go b/pkg/gallery/gallery.go
index 6202529a..0e9daa79 100644
--- a/pkg/gallery/gallery.go
+++ b/pkg/gallery/gallery.go
@@ -25,11 +25,11 @@ func InstallModelFromGallery(galleries []Gallery, name string, basePath string,
 
 		var config Config
 
 		if len(model.URL) > 0 {
 			var err error
-			config, err = GetGalleryConfigFromURL(model.URL)
+			config, err = GetGalleryConfigFromURL(model.URL, basePath)
 			if err != nil {
 				return err
 			}
 		} else if len(model.ConfigFile) > 0 {
 			// TODO: is this worse than using the override method with a blank cfg yaml?
@@ -140,13 +140,13 @@ func AvailableGalleryModels(galleries []Gallery, basePath string) ([]*GalleryMod
 	}
 
 	return models, nil
 }
 
-func findGalleryURLFromReferenceURL(url string) (string, error) {
+func findGalleryURLFromReferenceURL(url string, basePath string) (string, error) {
 	var refFile string
-	err := downloader.GetURI(url, func(url string, d []byte) error {
+	err := downloader.GetURI(url, basePath, func(url string, d []byte) error {
 		refFile = string(d)
 		if len(refFile) == 0 {
 			return fmt.Errorf("invalid reference file at url %s: %s", url, d)
 		}
 		cutPoint := strings.LastIndex(url, "/")
@@ -159,17 +159,17 @@ func findGalleryURLFromReferenceURL(url string) (string, error) {
 func getGalleryModels(gallery Gallery, basePath string) ([]*GalleryModel, error) {
 	var models []*GalleryModel = []*GalleryModel{}
 
 	if strings.HasSuffix(gallery.URL, ".ref") {
 		var err error
-		gallery.URL, err = findGalleryURLFromReferenceURL(gallery.URL)
+		gallery.URL, err = findGalleryURLFromReferenceURL(gallery.URL, basePath)
 		if err != nil {
 			return models, err
 		}
 	}
 
-	err := downloader.GetURI(gallery.URL, func(url string, d []byte) error {
+	err := downloader.GetURI(gallery.URL, basePath, func(url string, d []byte) error {
 		return yaml.Unmarshal(d, &models)
 	})
 	if err != nil {
 		if yamlErr, ok := err.(*yaml.TypeError); ok {
 			log.Debug().Msgf("YAML errors: %s\n\nwreckage of models: %+v", strings.Join(yamlErr.Errors, "\n"), models)
diff --git a/pkg/gallery/models.go b/pkg/gallery/models.go
index e697fcd6..225097c0 100644
--- a/pkg/gallery/models.go
+++ b/pkg/gallery/models.go
@@ -61,13 +61,13 @@ type File struct {
 type PromptTemplate struct {
 	Name    string `yaml:"name"`
 	Content string `yaml:"content"`
 }
 
-func GetGalleryConfigFromURL(url string) (Config, error) {
+func GetGalleryConfigFromURL(url string, basePath string) (Config, error) {
 	var config Config
-	err := downloader.GetURI(url, func(url string, d []byte) error {
+	err := downloader.GetURI(url, basePath, func(url string, d []byte) error {
 		return yaml.Unmarshal(d, &config)
 	})
 	if err != nil {
 		log.Error().Err(err).Str("url", url).Msg("failed to get gallery config for url")
 		return config, err
diff --git a/pkg/gallery/request_test.go b/pkg/gallery/request_test.go
index a9d54e32..af085e81 100644
--- a/pkg/gallery/request_test.go
+++ b/pkg/gallery/request_test.go
@@ -8,11 +8,11 @@ import (
 
 var _ = Describe("Gallery API tests", func() {
 	Context("requests", func() {
 		It("parses github with a branch", func() {
 			req := GalleryModel{URL: "github:go-skynet/model-gallery/gpt4all-j.yaml@main"}
-			e, err := GetGalleryConfigFromURL(req.URL)
+			e, err := GetGalleryConfigFromURL(req.URL, "")
 			Expect(err).ToNot(HaveOccurred())
 			Expect(e.Name).To(Equal("gpt4all-j"))
 		})
 	})
 })
diff --git a/pkg/startup/model_preload.go b/pkg/startup/model_preload.go
index d267d846..240fc6bd 100644
--- a/pkg/startup/model_preload.go
+++ b/pkg/startup/model_preload.go
@@ -18,11 +18,11 @@ func PreloadModelsConfigurations(modelLibraryURL string, modelPath string, model
 	for _, url := range models {
 
 		// As a best effort, try to resolve the model from the remote library
 		// if it's not resolved we try with the other method below
 		if modelLibraryURL != "" {
-			lib, err := embedded.GetRemoteLibraryShorteners(modelLibraryURL)
+			lib, err := embedded.GetRemoteLibraryShorteners(modelLibraryURL, modelPath)
 			if err == nil {
 				if lib[url] != "" {
 					log.Debug().Msgf("[startup] model configuration is defined remotely: %s (%s)", url, lib[url])
 					url = lib[url]
 				}

Vulnerable code: 	downloader.GetURI(url, func(url string, i []byte) error {
	config, err := gallery.GetGalleryConfigFromURL(req.URL)
func GetRemoteLibraryShorteners(url string) (map[string]string, error) {
	err := downloader.GetURI(url, func(_ string, i []byte) error {
func GetURI(url string, f func(url string, i []byte) error) error {
				GetURI("github:go-skynet/model-gallery/gpt4all-j.yaml", func(url string, i []byte) error {
				GetURI("github:go-skynet/model-gallery/gpt4all-j.yaml@main", func(url string, i []byte) error {
				GetURI("https://raw.githubusercontent.com/go-skynet/model-gallery/main/gpt4all-j.yaml", func(url string, i []byte) error {
			config, err = GetGalleryConfigFromURL(model.URL)
func findGalleryURLFromReferenceURL(url string) (string, error) {
	err := downloader.GetURI(url, func(url string, d []byte) error {
		gallery.URL, err = findGalleryURLFromReferenceURL(gallery.URL)
	err := downloader.GetURI(gallery.URL, func(url string, d []byte) error {
func GetGalleryConfigFromURL(url string) (Config, error) {
	err := downloader.GetURI(url, func(url string, d []byte) error {
			e, err := GetGalleryConfigFromURL(req.URL)
			lib, err := embedded.GetRemoteLibraryShorteners(modelLibraryURL)
Fixed code: 	// TODO: No tests currently seem to exercise file:// urls. Fix?
	downloader.GetURI(url, "", func(url string, i []byte) error {
	config, err := gallery.GetGalleryConfigFromURL(req.URL, modelPath)
func GetRemoteLibraryShorteners(url string, basePath string) (map[string]string, error) {
	err := downloader.GetURI(url, basePath, func(_ string, i []byte) error {
func GetURI(url string, basePath string, f func(url string, i []byte) error) error {
		// Check if the local file is rooted in basePath
		err = utils.VerifyPath(resolvedFile, basePath)
		if err != nil {
			return err
		}
				GetURI("github:go-skynet/model-gallery/gpt4all-j.yaml", "", func(url string, i []byte) error {
				GetURI("github:go-skynet/model-gallery/gpt4all-j.yaml@main", "", func(url string, i []byte) error {
				GetURI("https://raw.githubusercontent.com/go-skynet/model-gallery/main/gpt4all-j.yaml", "", func(url string, i []byte) error {
			config, err = GetGalleryConfigFromURL(model.URL, basePath)
func findGalleryURLFromReferenceURL(url string, basePath string) (string, error) {
	err := downloader.GetURI(url, basePath, func(url string, d []byte) error {
		gallery.URL, err = findGalleryURLFromReferenceURL(gallery.URL, basePath)
	err := downloader.GetURI(gallery.URL, basePath, func(url string, d []byte) error {
func GetGalleryConfigFromURL(url string, basePath string) (Config, error) {
	err := downloader.GetURI(url, basePath, func(url string, d []byte) error {
			e, err := GetGalleryConfigFromURL(req.URL, "")
			lib, err := embedded.GetRemoteLibraryShorteners(modelLibraryURL, modelPath)