mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Ignore symlinked folders when verifying plugin signature (#34434)
* add check + test * fix test * add manifest * fix linter * add nolint * separate err cond checks * only collect relevant plugin files * skip symlinks * refactor * add missing test files + enable scanning Chromium.app/ * remove test since case already covered * remove unnecessary changes from before * refactor * remove comment
This commit is contained in:
parent
3900005bf1
commit
303352a89b
@ -530,7 +530,7 @@ func (s *PluginScanner) walker(currentPath string, f os.FileInfo, err error) err
|
|||||||
return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err)
|
return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Name() == "node_modules" || f.Name() == "Chromium.app" {
|
if f.Name() == "node_modules" {
|
||||||
return util.ErrWalkSkipDir
|
return util.ErrWalkSkipDir
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -577,12 +577,6 @@ func (s *PluginScanner) loadPlugin(pluginJSONFilePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pluginCommon.PluginDir = filepath.Dir(pluginJSONFilePath)
|
pluginCommon.PluginDir = filepath.Dir(pluginJSONFilePath)
|
||||||
pluginCommon.Files, err = collectPluginFilesWithin(pluginCommon.PluginDir)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Warn("Could not collect plugin file information in directory", "pluginID", pluginCommon.Id, "dir", pluginCommon.PluginDir)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
signatureState, err := getPluginSignatureState(s.log, &pluginCommon)
|
signatureState, err := getPluginSignatureState(s.log, &pluginCommon)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Warn("Could not get plugin signature state", "pluginID", pluginCommon.Id, "err", err)
|
s.log.Warn("Could not get plugin signature state", "pluginID", pluginCommon.Id, "err", err)
|
||||||
@ -726,25 +720,6 @@ func (pm *PluginManager) GetPluginMarkdown(pluginId string, name string) ([]byte
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// gets plugin filenames that require verification for plugin signing
|
|
||||||
func collectPluginFilesWithin(rootDir string) ([]string, error) {
|
|
||||||
var files []string
|
|
||||||
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !info.IsDir() && info.Name() != "MANIFEST.txt" {
|
|
||||||
file, err := filepath.Rel(rootDir, path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
files = append(files, filepath.ToSlash(file))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return files, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *PluginManager) StaticRoutes() []*plugins.PluginStaticRoute {
|
func (pm *PluginManager) StaticRoutes() []*plugins.PluginStaticRoute {
|
||||||
return pm.staticRoutes
|
return pm.staticRoutes
|
||||||
}
|
}
|
||||||
|
@ -371,7 +371,7 @@ func TestPluginManager_Init(t *testing.T) {
|
|||||||
assert.Nil(t, pm.plugins[("test")])
|
assert.Nil(t, pm.plugins[("test")])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("With back-end plugin with a lib dir that has symbolic links", func(t *testing.T) {
|
t.Run("With plugin that contains symlink file + directory", func(t *testing.T) {
|
||||||
origAppURL := setting.AppUrl
|
origAppURL := setting.AppUrl
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
setting.AppUrl = origAppURL
|
setting.AppUrl = origAppURL
|
||||||
@ -379,23 +379,25 @@ func TestPluginManager_Init(t *testing.T) {
|
|||||||
setting.AppUrl = defaultAppURL
|
setting.AppUrl = defaultAppURL
|
||||||
|
|
||||||
pm := createManager(t, func(pm *PluginManager) {
|
pm := createManager(t, func(pm *PluginManager) {
|
||||||
pm.Cfg.PluginsPath = "testdata/symbolic-file-links"
|
pm.Cfg.PluginsPath = "testdata/includes-symlinks"
|
||||||
})
|
})
|
||||||
err := pm.Init()
|
err := pm.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
// This plugin should be properly registered, even though it has a symbolicly linked file in it.
|
|
||||||
require.Empty(t, pm.scanningErrors)
|
require.Empty(t, pm.scanningErrors)
|
||||||
const pluginID = "test"
|
|
||||||
assert.NotNil(t, pm.plugins[pluginID])
|
const pluginID = "test-app"
|
||||||
assert.Equal(t, "datasource", pm.plugins[pluginID].Type)
|
p := pm.GetPlugin(pluginID)
|
||||||
assert.Equal(t, "Test", pm.plugins[pluginID].Name)
|
|
||||||
assert.Equal(t, pluginID, pm.plugins[pluginID].Id)
|
assert.NotNil(t, p)
|
||||||
assert.Equal(t, "1.0.0", pm.plugins[pluginID].Info.Version)
|
assert.NotNil(t, pm.GetApp(pluginID))
|
||||||
assert.Equal(t, plugins.PluginSignatureValid, pm.plugins[pluginID].Signature)
|
assert.Equal(t, pluginID, p.Id)
|
||||||
assert.Equal(t, plugins.GrafanaType, pm.plugins[pluginID].SignatureType)
|
assert.Equal(t, "app", p.Type)
|
||||||
assert.Equal(t, "Grafana Labs", pm.plugins[pluginID].SignatureOrg)
|
assert.Equal(t, "Test App", p.Name)
|
||||||
assert.False(t, pm.plugins[pluginID].IsCorePlugin)
|
assert.Equal(t, "1.0.0", p.Info.Version)
|
||||||
assert.NotNil(t, pm.plugins[("test")])
|
assert.Equal(t, plugins.PluginSignatureValid, p.Signature)
|
||||||
|
assert.Equal(t, plugins.GrafanaType, p.SignatureType)
|
||||||
|
assert.Equal(t, "Grafana Labs", p.SignatureOrg)
|
||||||
|
assert.False(t, p.IsCorePlugin)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("With back-end plugin that is symlinked to plugins dir", func(t *testing.T) {
|
t.Run("With back-end plugin that is symlinked to plugins dir", func(t *testing.T) {
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -210,9 +211,17 @@ func getPluginSignatureState(log log.Logger, plugin *plugins.PluginBase) (plugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if manifest.isV2() {
|
if manifest.isV2() {
|
||||||
|
pluginFiles, err := pluginFilesRequiringVerification(plugin)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Could not collect plugin file information in directory", "pluginID", plugin.Id, "dir", plugin.PluginDir)
|
||||||
|
return plugins.PluginSignatureState{
|
||||||
|
Status: plugins.PluginSignatureInvalid,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
// Track files missing from the manifest
|
// Track files missing from the manifest
|
||||||
var unsignedFiles []string
|
var unsignedFiles []string
|
||||||
for _, f := range plugin.Files {
|
for _, f := range pluginFiles {
|
||||||
if _, exists := manifestFiles[f]; !exists {
|
if _, exists := manifestFiles[f]; !exists {
|
||||||
unsignedFiles = append(unsignedFiles, f)
|
unsignedFiles = append(unsignedFiles, f)
|
||||||
}
|
}
|
||||||
@ -234,3 +243,60 @@ func getPluginSignatureState(log log.Logger, plugin *plugins.PluginBase) (plugin
|
|||||||
SigningOrg: manifest.SignedByOrgName,
|
SigningOrg: manifest.SignedByOrgName,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gets plugin filenames that require verification for plugin signing
|
||||||
|
// returns filenames as a slice of posix style paths relative to plugin directory
|
||||||
|
func pluginFilesRequiringVerification(plugin *plugins.PluginBase) ([]string, error) {
|
||||||
|
var files []string
|
||||||
|
err := filepath.Walk(plugin.PluginDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||||
|
symlinkPath, err := filepath.EvalSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
symlink, err := os.Stat(symlinkPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip symlink directories
|
||||||
|
if symlink.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that symlinked file is within plugin directory
|
||||||
|
p, err := filepath.Rel(plugin.PluginDir, symlinkPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(p, ".."+string(filepath.Separator)) {
|
||||||
|
return fmt.Errorf("file '%s' not inside of plugin directory", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip directories and MANIFEST.txt
|
||||||
|
if info.IsDir() || info.Name() == "MANIFEST.txt" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that file is within plugin directory
|
||||||
|
file, err := filepath.Rel(plugin.PluginDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(file, ".."+string(filepath.Separator)) {
|
||||||
|
return fmt.Errorf("file '%s' not inside of plugin directory", file)
|
||||||
|
}
|
||||||
|
|
||||||
|
files = append(files, filepath.ToSlash(file))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return files, err
|
||||||
|
}
|
||||||
|
31
pkg/plugins/manager/testdata/includes-symlinks/MANIFEST.txt
vendored
Normal file
31
pkg/plugins/manager/testdata/includes-symlinks/MANIFEST.txt
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
-----BEGIN PGP SIGNED MESSAGE-----
|
||||||
|
Hash: SHA512
|
||||||
|
|
||||||
|
{
|
||||||
|
"manifestVersion": "2.0.0",
|
||||||
|
"signatureType": "grafana",
|
||||||
|
"signedByOrg": "grafana",
|
||||||
|
"signedByOrgName": "Grafana Labs",
|
||||||
|
"plugin": "test-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"time": 1622547655175,
|
||||||
|
"keyId": "7e4d0c6a708866e7",
|
||||||
|
"files": {
|
||||||
|
"symlink_to_txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048",
|
||||||
|
"plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf",
|
||||||
|
"dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303",
|
||||||
|
"dashboards/extra/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee",
|
||||||
|
"text.txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
Version: OpenPGP.js v4.10.1
|
||||||
|
Comment: https://openpgpjs.org
|
||||||
|
|
||||||
|
wqEEARMKAAYFAmC2HMcACgkQfk0ManCIZuddZgIJAVsIWg93v20C9baQhKth
|
||||||
|
G9f1PY900AnjVNK/494/qdR7UrBWDH0LWboVIkcALEawo6oLMUzyuMIPknCy
|
||||||
|
QW45HwlQAgjRW2Wm1uxcFN6164M5UHAR9+a2mHrpva18bG6ELYliLRFsuEnj
|
||||||
|
WG0SOCjmUoSG/qN6iREuLxii5xK4Levu158kCg==
|
||||||
|
=Mpyz
|
||||||
|
-----END PGP SIGNATURE-----
|
29
pkg/plugins/manager/testdata/includes-symlinks/dashboards/connections.json
vendored
Normal file
29
pkg/plugins/manager/testdata/includes-symlinks/dashboards/connections.json
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"__inputs": [
|
||||||
|
{
|
||||||
|
"name": "DS_NAME",
|
||||||
|
"type": "datasource",
|
||||||
|
"pluginId": "graphite"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"uid": "1MHHlVjzz",
|
||||||
|
"title": "Nginx Connections",
|
||||||
|
"revision": 25,
|
||||||
|
"schemaVersion": 11,
|
||||||
|
"tags": ["tag1", "tag2"],
|
||||||
|
"number_array": [1,2,3,10.33],
|
||||||
|
"boolean_true": true,
|
||||||
|
"boolean_false": false,
|
||||||
|
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"type": "graph",
|
||||||
|
"datasource": "${DS_NAME}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5
pkg/plugins/manager/testdata/includes-symlinks/dashboards/extra/memory.json
vendored
Normal file
5
pkg/plugins/manager/testdata/includes-symlinks/dashboards/extra/memory.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"title": "Nginx Memory",
|
||||||
|
"revision": 2,
|
||||||
|
"schemaVersion": 11
|
||||||
|
}
|
1
pkg/plugins/manager/testdata/includes-symlinks/extra
vendored
Symbolic link
1
pkg/plugins/manager/testdata/includes-symlinks/extra
vendored
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
dashboards/extra
|
55
pkg/plugins/manager/testdata/includes-symlinks/plugin.json
vendored
Normal file
55
pkg/plugins/manager/testdata/includes-symlinks/plugin.json
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"type": "app",
|
||||||
|
"name": "Test App",
|
||||||
|
"id": "test-app",
|
||||||
|
|
||||||
|
"staticRoot": ".",
|
||||||
|
|
||||||
|
"pages": [
|
||||||
|
{ "name": "Live stream", "component": "StreamPageCtrl", "role": "Editor"},
|
||||||
|
{ "name": "Log view", "component": "LogsPageCtrl", "role": "Viewer"}
|
||||||
|
],
|
||||||
|
|
||||||
|
"css": {
|
||||||
|
"dark": "css/dark.css",
|
||||||
|
"light": "css/light.css"
|
||||||
|
},
|
||||||
|
|
||||||
|
"info": {
|
||||||
|
"description": "Official Grafana Test App & Dashboard bundle",
|
||||||
|
"author": {
|
||||||
|
"name": "Test Inc.",
|
||||||
|
"url": "http://test.com"
|
||||||
|
},
|
||||||
|
"keywords": ["test"],
|
||||||
|
"logos": {
|
||||||
|
"small": "img/logo_small.png",
|
||||||
|
"large": "img/logo_large.png"
|
||||||
|
},
|
||||||
|
"screenshots": [
|
||||||
|
{"name": "img1", "path": "img/screenshot1.png"},
|
||||||
|
{"name": "img2", "path": "img/screenshot2.png"}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{"name": "Project site", "url": "http://project.com"},
|
||||||
|
{"name": "License & Terms", "url": "http://license.com"}
|
||||||
|
],
|
||||||
|
"version": "1.0.0",
|
||||||
|
"updated": "2015-02-10"
|
||||||
|
},
|
||||||
|
|
||||||
|
"includes": [
|
||||||
|
{"type": "dashboard", "name": "Nginx Connections", "path": "dashboards/connections.json"},
|
||||||
|
{"type": "dashboard", "name": "Nginx Memory", "path": "dashboards/memory.json"},
|
||||||
|
{"type": "panel", "name": "Nginx Panel"},
|
||||||
|
{"type": "datasource", "name": "Nginx Datasource"}
|
||||||
|
],
|
||||||
|
|
||||||
|
"dependencies": {
|
||||||
|
"grafanaVersion": "3.x.x",
|
||||||
|
"plugins": [
|
||||||
|
{"type": "datasource", "id": "graphite", "name": "Graphite", "version": "1.0.0"},
|
||||||
|
{"type": "panel", "id": "graph", "name": "Graph", "version": "1.0.0"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
1
pkg/plugins/manager/testdata/includes-symlinks/symlink_to_txt
vendored
Symbolic link
1
pkg/plugins/manager/testdata/includes-symlinks/symlink_to_txt
vendored
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
text.txt
|
8
pkg/plugins/manager/testdata/includes-symlinks/text.txt
vendored
Normal file
8
pkg/plugins/manager/testdata/includes-symlinks/text.txt
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
eafafewfewfwef
|
||||||
|
fawfwfwefewfwaef
|
||||||
|
fawewfewfewfewfe
|
||||||
|
wafewafewfewafewaf
|
||||||
|
efewafeawfewafewf
|
||||||
|
ewafewafewfeawfawf
|
||||||
|
weafweafewfefewafewa
|
||||||
|
fewafweafwafeaf
|
@ -1,32 +0,0 @@
|
|||||||
|
|
||||||
-----BEGIN PGP SIGNED MESSAGE-----
|
|
||||||
Hash: SHA512
|
|
||||||
|
|
||||||
{
|
|
||||||
"manifestVersion": "2.0.0",
|
|
||||||
"signatureType": "grafana",
|
|
||||||
"signedByOrg": "grafana",
|
|
||||||
"signedByOrgName": "Grafana Labs",
|
|
||||||
"rootUrls": [
|
|
||||||
"http://localhost:3000/"
|
|
||||||
],
|
|
||||||
"plugin": "test",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"time": 1623327375936,
|
|
||||||
"keyId": "7e4d0c6a708866e7",
|
|
||||||
"files": {
|
|
||||||
"plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f",
|
|
||||||
"lib/somelib.so.1": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
||||||
"lib/somelib.so": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-----BEGIN PGP SIGNATURE-----
|
|
||||||
Version: OpenPGP.js v4.10.1
|
|
||||||
Comment: https://openpgpjs.org
|
|
||||||
|
|
||||||
wqEEARMKAAYFAmDCApAACgkQfk0ManCIZuc+JAIJAVpL9/0Q1vnQyB+0MaOJ
|
|
||||||
oklt6Kw7/8OyL0oOj3lG0eW3qkJSOUfdM7Si2VW/BudruyUQovpU3EXr00/A
|
|
||||||
oR1SZtoLAgdNyc9KldvPNV95XAsNz8jjBzn12G2Qer4/Vgjzpl5wvn2WvZ1l
|
|
||||||
/NFR8rwHVhvMyGe7c1PJ6Gt6KpbQZ5j20j1NMA==
|
|
||||||
=GIHH
|
|
||||||
-----END PGP SIGNATURE-----
|
|
@ -1 +0,0 @@
|
|||||||
somelib.so.1
|
|
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "datasource",
|
|
||||||
"name": "Test",
|
|
||||||
"id": "test",
|
|
||||||
"backend": true,
|
|
||||||
"executable": "test",
|
|
||||||
"state": "alpha",
|
|
||||||
"info": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Test",
|
|
||||||
"author": {
|
|
||||||
"name": "Will Browne",
|
|
||||||
"url": "https://willbrowne.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -71,7 +71,6 @@ type PluginBase struct {
|
|||||||
PluginDir string `json:"-"`
|
PluginDir string `json:"-"`
|
||||||
DefaultNavUrl string `json:"-"`
|
DefaultNavUrl string `json:"-"`
|
||||||
IsCorePlugin bool `json:"-"`
|
IsCorePlugin bool `json:"-"`
|
||||||
Files []string `json:"-"`
|
|
||||||
SignatureType PluginSignatureType `json:"-"`
|
SignatureType PluginSignatureType `json:"-"`
|
||||||
SignatureOrg string `json:"-"`
|
SignatureOrg string `json:"-"`
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ func Walk(path string, followSymlinks bool, detectSymlinkInfiniteLoop bool, walk
|
|||||||
// If symlinkPathsFollowed is not nil, then we need to detect infinite loop.
|
// If symlinkPathsFollowed is not nil, then we need to detect infinite loop.
|
||||||
func walk(path string, info os.FileInfo, resolvedPath string, symlinkPathsFollowed map[string]bool, walkFn WalkFunc) error {
|
func walk(path string, info os.FileInfo, resolvedPath string, symlinkPathsFollowed map[string]bool, walkFn WalkFunc) error {
|
||||||
if info == nil {
|
if info == nil {
|
||||||
return errors.New("Walk: Nil FileInfo passed")
|
return errors.New("walk: Nil FileInfo passed")
|
||||||
}
|
}
|
||||||
err := walkFn(resolvedPath, info, nil)
|
err := walkFn(resolvedPath, info, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user