Coordinated Disclosure Timeline

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, &notFound) {
      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

CVE

Resources

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.