CVE-2021-43815: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Grafana is an open-source platform for monitoring and observability. Grafana prior to versions 8.3.2 and 7.5.12 has a directory traversal for arbitrary .csv files. It only affects instances that have the developer testing tool called TestData DB data source enabled and configured. The vulnerability is limited in scope, and only allows access to files with the extension .csv to authenticated users only. Grafana Cloud instances have not been affected by the vulnerability. Versions 8.3.2 and 7.5.12 contain a patch for this issue. There is a workaround available for users who cannot upgrade. Running a reverse proxy in front of Grafana that normalizes the PATH of the request will mitigate the vulnerability. The proxy will have to also be able to handle url encoded paths.
CWE 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.
CVSS v3 base score: 4.3
Published: 2021-12-10T21:15Z
Repository: https://github.com/grafana/grafana
Language: Go
Files changed: pkg/api/plugins.go, pkg/tsdb/testdatasource/csv_data.go
Commit date: 2021-12-10 16:10:04 +0100
Commit message: Backport fix to main (#42979)

* fixes

(cherry picked from commit a2c386915ce11b9422f4af8ae181eaa1a22bc5c3)
(cherry picked from commit 06706efbbe59ad9d3075835cc31e2f734e36df95)

* fix regex

(cherry picked from commit a259213a3badc9618e969f2c8db0a0143f00faee)
(cherry picked from commit 1d7105c0959df2083814237024f7ec098a76099b)

* lint (#42970)

(cherry picked from commit afb9e8e5f3c33fa59c5a1053d98d8d97815af002)

Co-authored-by: Will Browne <will.browne@grafana.com>
Co-authored-by: malcolmholmes <42545407+malcolmholmes@users.noreply.github.com>
Diff: diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go
index ca076671e3..7cf31dedd4 100644
--- a/pkg/api/plugins.go
+++ b/pkg/api/plugins.go
@@ -488,19 +488,19 @@ func (hs *HTTPServer) pluginMarkdown(ctx context.Context, pluginId string, name
 	if !exists {
 		return nil, plugins.NotFoundError{PluginID: pluginId}
 	}
 
 	// nolint:gosec
-	// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
-	// on plugin the folder structure on disk and not user input.
-	path := filepath.Join(plugin.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
+	// We can ignore the gosec G304 warning since we have cleaned the requested file path and subsequently
+	// use this with a prefix of the plugin's directory, which is set during plugin loading
+	path := filepath.Join(plugin.PluginDir, mdFilepath(strings.ToUpper(name)))
 	exists, err := fs.Exists(path)
 	if err != nil {
 		return nil, err
 	}
 	if !exists {
-		path = filepath.Join(plugin.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
+		path = filepath.Join(plugin.PluginDir, mdFilepath(strings.ToLower(name)))
 	}
 
 	exists, err = fs.Exists(path)
 	if err != nil {
 		return nil, err
@@ -508,13 +508,17 @@ func (hs *HTTPServer) pluginMarkdown(ctx context.Context, pluginId string, name
 	if !exists {
 		return make([]byte, 0), nil
 	}
 
 	// nolint:gosec
-	// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
-	// on plugin the folder structure on disk and not user input.
+	// We can ignore the gosec G304 warning since we have cleaned the requested file path and subsequently
+	// use this with a prefix of the plugin's directory, which is set during plugin loading
 	data, err := ioutil.ReadFile(path)
 	if err != nil {
 		return nil, err
 	}
 	return data, nil
 }
+
+func mdFilepath(mdFilename string) string {
+	return filepath.Clean(filepath.Join("/", fmt.Sprintf("%s.md", mdFilename)))
+}
diff --git a/pkg/tsdb/testdatasource/csv_data.go b/pkg/tsdb/testdatasource/csv_data.go
index 2c1b56517a..78641c0c31 100644
--- a/pkg/tsdb/testdatasource/csv_data.go
+++ b/pkg/tsdb/testdatasource/csv_data.go
@@ -75,17 +75,18 @@ func (s *Service) handleCsvFileScenario(ctx context.Context, req *backend.QueryD
 
 	return resp, nil
 }
 
 func (s *Service) loadCsvFile(fileName string) (*data.Frame, error) {
-	validFileName := regexp.MustCompile(`([\w_]+)\.csv`)
+	validFileName := regexp.MustCompile(`^\w+\.csv$`)
 
 	if !validFileName.MatchString(fileName) {
 		return nil, fmt.Errorf("invalid csv file name: %q", fileName)
 	}
 
-	filePath := filepath.Join(s.cfg.StaticRootPath, "testdata", fileName)
+	csvFilepath := filepath.Clean(filepath.Join("/", fileName))
+	filePath := filepath.Join(s.cfg.StaticRootPath, "testdata", csvFilepath)
 
 	// Can ignore gosec G304 here, because we check the file pattern above
 	// nolint:gosec
 	fileReader, err := os.Open(filePath)
 	if err != nil {

Vulnerable code: 	// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
	// on plugin the folder structure on disk and not user input.
	path := filepath.Join(plugin.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
		path = filepath.Join(plugin.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
	// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
	// on plugin the folder structure on disk and not user input.
	validFileName := regexp.MustCompile(`([\w_]+)\.csv`)
	filePath := filepath.Join(s.cfg.StaticRootPath, "testdata", fileName)
Fixed code: 	// We can ignore the gosec G304 warning since we have cleaned the requested file path and subsequently
	// use this with a prefix of the plugin's directory, which is set during plugin loading
	path := filepath.Join(plugin.PluginDir, mdFilepath(strings.ToUpper(name)))
		path = filepath.Join(plugin.PluginDir, mdFilepath(strings.ToLower(name)))
	// We can ignore the gosec G304 warning since we have cleaned the requested file path and subsequently
	// use this with a prefix of the plugin's directory, which is set during plugin loading

func mdFilepath(mdFilename string) string {
	return filepath.Clean(filepath.Join("/", fmt.Sprintf("%s.md", mdFilename)))
}
	validFileName := regexp.MustCompile(`^\w+\.csv$`)
	csvFilepath := filepath.Clean(filepath.Join("/", fileName))
	filePath := filepath.Join(s.cfg.StaticRootPath, "testdata", csvFilepath)