Coordinated Disclosure Timeline
- 2021-12-09: Report sent to security@grafana.com
- 2021-12-09: Issue is acknowledged
- 2021-12-10: Issue is fixed
Summary
A path traversal vulnerability was found in Grafana REST API
Product
Grafana
Tested Version
v8.3.1
Details
Path traversal (GHSL-2021-1053
)
The GetPluginMarkdown
request handler is vulnerable to path traversal attacks.
func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response {
pluginID := web.Params(c.Req)[":pluginId"]
name := web.Params(c.Req)[":name"] // [1]
content, err := hs.pluginMarkdown(c.Req.Context(), pluginID, name) // [2]
if err != nil {
var notFound plugins.NotFoundError
if errors.As(err, ¬Found) {
return response.Error(404, notFound.Error(), nil)
}
return response.Error(500, "Could not get markdown file", err)
}
// fallback try readme
if len(content) == 0 {
content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, "readme")
if err != nil {
return response.Error(501, "Could not get markdown file", err)
}
}
resp := response.Respond(200, content) // [5]
resp.SetHeader("Content-Type", "text/plain; charset=utf-8")
return resp
}
The request handler is mapped to the /plugins/:pluginId/markdown/:name
endpoint in pkg/api/api.go
:
apiRoute.Get("/plugins/:pluginId/markdown/:name", routing.Wrap(hs.GetPluginMarkdown))
Even though this endpoint requires authentication, any low priviledged user (eg: VIEWER
) can abuse this vulnerability to read arbitrary markdown files in the server.
The untrusted data enters the application at [1] and is later passed to pluginMarkdown
at [2].
func (hs *HTTPServer) pluginMarkdown(ctx context.Context, pluginId string, name string) ([]byte, error) {
plugin, exists := hs.pluginStore.Plugin(ctx, pluginId)
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))) // [3]
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)))
}
exists, err = fs.Exists(path)
if err != nil {
return nil, err
}
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.
data, err := ioutil.ReadFile(path) // [4]
if err != nil {
return nil, err
}
return data, nil
}
The arbitrary name
is then appended the .md
extension and concatenated to plugin.PluginDir
at [3]. Finally the file’s contents are read at [4] and returned to the caller function where they are returned in the HTTP response at [5]
Impact
This issue may lead to arbitrary .md file disclosure.
PoC
- Start Grafana 8.3.1 on a docker container:
docker run -d -p 3000:3000 --name grafana grafana/grafana-oss:8.3.1
- Visit
localhost:3000
, and authenticate withadmin
/admin
- Create a new
VIEWER
user calledfoo
with passwordfooo
- Create
/tmp/foo.md
with arbitrary contents - Request
/tmp/foo.md
usingcurl http://localhost:3000/api/plugins/alertlist/markdown/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2ftmp%2ffoo -u foo:fooo
CVE
- CVE-2021-43813
- CVE-2021-43815
Resources
- https://github.com/grafana/grafana/security/advisories/GHSA-c3q8-26ph-9g2q
- https://github.com/grafana/grafana/security/advisories/GHSA-7533-c8qv-jm9m
Credit
This issue was discovered and reported by GHSL team member @pwntester (Alvaro Muñoz).
Contact
You can contact the GHSL team at securitylab@github.com
, please include a reference to GHSL-2021-1053
in any communication regarding this issue.