CVE-2021-43798 (published 2021-12-07T19:15Z)

Grafana is an open-source platform for monitoring and observability. Grafana versions 8.0.0-beta1 through 8.3.0 (except for patched versions) iss vulnerable to directory traversal, allowing access to local files. The vulnerable URL path is: `<grafana_host_url>/public/plugins//`, where is the plugin ID for any installed plugin. At no time has Grafana Cloud been vulnerable. Users are advised to upgrade to patched versions 8.0.7, 8.1.8, 8.2.7, or 8.3.1. The GitHub Security Advisory contains more information about vulnerable URL paths, mitigation, and the disclosure timeline.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/grafana/grafana
commit_details: repo_url: https://github.com/grafana/grafana
author: Dimitris Sotirakis
author_date: 2021-12-07 19:15:53+02:00
msg: Security: Fix directory traversal issue (#42846)

* security: fix dir traversal issue

(cherry picked from commit 00e38ba555cfb120361c9623de3285d70c60172f)

* Improve comments and error message.

Co-authored-by: Kyle Brandt <kyle@grafana.com>
repository: repo_url: https://github.com/grafana/grafana
repo_name: grafana/grafana
description: The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more. 
repo_language: TypeScript
owner: grafana
file_changes: filename: plugins.go
change_type: ModificationType.MODIFY
diff: @@ -289,14 +289,27 @@ func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) {
 		return
 	}
 
-	requestedFile := filepath.Clean(web.Params(c.Req)["*"])
-	pluginFilePath := filepath.Join(plugin.PluginDir, requestedFile)
+	// prepend slash for cleaning relative paths
+	requestedFile := filepath.Clean(filepath.Join("/", web.Params(c.Req)["*"]))
+	rel, err := filepath.Rel("/", requestedFile)
+	if err != nil {
+		// slash is prepended above therefore this is not expected to fail
+		c.JsonApiErr(500, "Failed to get the relative path", err)
+		return
+	}
 
-	if !plugin.IncludedInSignature(requestedFile) {
+	if !plugin.IncludedInSignature(rel) {
 		hs.log.Warn("Access to requested plugin file will be forbidden in upcoming Grafana versions as the file "+
 			"is not included in the plugin signature", "file", requestedFile)
 	}
 
+	absPluginDir, err := filepath.Abs(plugin.PluginDir)
+	if err != nil {
+		c.JsonApiErr(500, "Failed to get plugin absolute path", nil)
+		return
+	}
+
+	pluginFilePath := filepath.Join(absPluginDir, rel)
 	// It's safe to ignore gosec warning G304 since we already clean the requested file path and subsequently
 	// use this with a prefix of the plugin's directory, which is set during plugin loading
 	// nolint:gosec

programming_language: Go
filename: plugins_test.go
change_type: ModificationType.MODIFY
diff: @@ -23,9 +23,13 @@ func Test_GetPluginAssets(t *testing.T) {
 	pluginDir := "."
 	tmpFile, err := ioutil.TempFile(pluginDir, "")
 	require.NoError(t, err)
+	tmpFileInParentDir, err := ioutil.TempFile("..", "")
+	require.NoError(t, err)
 	t.Cleanup(func() {
 		err := os.RemoveAll(tmpFile.Name())
 		assert.NoError(t, err)
+		err = os.RemoveAll(tmpFileInParentDir.Name())
+		assert.NoError(t, err)
 	})
 	expectedBody := "Plugin test"
 	_, err = tmpFile.WriteString(expectedBody)
@@ -61,6 +65,29 @@ func Test_GetPluginAssets(t *testing.T) {
 			})
 	})
 
+	t.Run("Given a request for a relative path", func(t *testing.T) {
+		p := plugins.PluginDTO{
+			JSONData: plugins.JSONData{
+				ID: pluginID,
+			},
+			PluginDir: pluginDir,
+		}
+		service := &fakePluginStore{
+			plugins: map[string]plugins.PluginDTO{
+				pluginID: p,
+			},
+		}
+		l := &logger{}
+
+		url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, tmpFileInParentDir.Name())
+		pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l,
+			func(sc *scenarioContext) {
+				callGetPluginAsset(sc)
+
+				require.Equal(t, 404, sc.resp.Code)
+			})
+	})
+
 	t.Run("Given a request for an existing plugin file that is not listed as a signature covered file", func(t *testing.T) {
 		p := plugins.PluginDTO{
 			JSONData: plugins.JSONData{

programming_language: Go