Plugins: Add Subresource Integrity checks (#93024)
* Plugins: Pass hashes for SRI to frontend
* Add SRI hashes to frontendsettings DTOs
* Add docstring
* TestSriHashes
* Fix typo
* Changed SriHashes to ModuleHash
* update loader_test compareOpts
* update ModuleHash error message
* Add TestModuleHash/no_module.js
* Add omitEmpty to moduleHash
* Add ModuleHash to api/plugins/${pluginId}/settings
* moved ModuleHash field
* feat(plugins): add moduleHash to bootData and plugin types
* feat(plugins): if moduleHash is available apply it to systemjs importmap
* Calculate ModuleHash for CDN provisioned plugins
* Add ModuleHash tests for TestCalculate
* adjust test case name
* removed .envrc
* Fix signature verification failing for internal plugins
* fix tests
* Add pluginsFilesystemSriChecks feature togglemk
* renamed FilesystemSriChecksEnabled
* refactor(plugin_loader): prefer extending type declaration over ts-error
* added a couple more tests
* Removed unused features
* Removed unused argument from signature.DefaultCalculator call
* Removed unused argument from bootstrap.DefaultConstructFunc
* Moved ModuleHash to pluginassets service
* update docstring
* lint
* Removed cdn dependency from manifest.Signature
* add tests
* fix extra parameters in tests
* "fix" tests
* removed outdated test
* removed unused cdn dependency in signature.DefaultCalculator
* reduce diff
* Cache returned values
* Add support for deeply nested plugins (more than 1 hierarchy level)
* simplify cache usage
* refactor TestService_ModuleHash_Cache
* removed unused testdata
* re-generate feature toggles
* use version for module hash cache
* Renamed feature toggle to pluginsSriChecks and use it for both cdn and filesystem
* Removed app/types/system-integrity.d.ts
* re-generate feature toggles
* re-generate feature toggles
* feat(plugins): put systemjs integrity hash behind feature flag
---------
Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
This commit is contained in:
co-authored by
Jack Westbrook
parent
153036be2e
commit
0db65d229e
@@ -53,7 +53,7 @@ type PluginManifest struct {
|
||||
RootURLs []string `json:"rootUrls"`
|
||||
}
|
||||
|
||||
func (m *PluginManifest) isV2() bool {
|
||||
func (m *PluginManifest) IsV2() bool {
|
||||
return strings.HasPrefix(m.ManifestVersion, "2.")
|
||||
}
|
||||
|
||||
@@ -107,34 +107,17 @@ func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*Plugi
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) {
|
||||
if defaultSignature, exists := src.DefaultSignature(ctx); exists {
|
||||
return defaultSignature, nil
|
||||
}
|
||||
fsFiles, err := plugin.FS.Files()
|
||||
if err != nil {
|
||||
return plugins.Signature{}, fmt.Errorf("files: %w", err)
|
||||
}
|
||||
if len(fsFiles) == 0 {
|
||||
s.log.Warn("No plugin file information in directory", "pluginId", plugin.JSONData.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
}
|
||||
var ErrSignatureTypeUnsigned = errors.New("plugin is unsigned")
|
||||
|
||||
f, err := plugin.FS.Open("MANIFEST.txt")
|
||||
// ReadPluginManifestFromFS reads the plugin manifest from the provided plugins.FS.
|
||||
// If the manifest is not found, it will return an error wrapping ErrSignatureTypeUnsigned.
|
||||
func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS) (*PluginManifest, error) {
|
||||
f, err := pfs.Open("MANIFEST.txt")
|
||||
if err != nil {
|
||||
if errors.Is(err, plugins.ErrFileNotExist) {
|
||||
s.log.Debug("Could not find a MANIFEST.txt", "id", plugin.JSONData.ID, "error", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusUnsigned,
|
||||
}, nil
|
||||
return nil, fmt.Errorf("%w: could not find a MANIFEST.txt", ErrSignatureTypeUnsigned)
|
||||
}
|
||||
|
||||
s.log.Debug("Could not open MANIFEST.txt", "id", plugin.JSONData.ID, "error", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
return nil, fmt.Errorf("could not open MANIFEST.txt: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if f == nil {
|
||||
@@ -147,21 +130,47 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
|
||||
byteValue, err := io.ReadAll(f)
|
||||
if err != nil || len(byteValue) < 10 {
|
||||
s.log.Debug("MANIFEST.TXT is invalid", "id", plugin.JSONData.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusUnsigned,
|
||||
}, nil
|
||||
return nil, fmt.Errorf("%w: MANIFEST.txt is invalid", ErrSignatureTypeUnsigned)
|
||||
}
|
||||
|
||||
manifest, err := s.readPluginManifest(ctx, byteValue)
|
||||
if err != nil {
|
||||
s.log.Warn("Plugin signature invalid", "id", plugin.JSONData.ID, "error", err)
|
||||
return nil, err
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) {
|
||||
if defaultSignature, exists := src.DefaultSignature(ctx); exists {
|
||||
return defaultSignature, nil
|
||||
}
|
||||
|
||||
manifest, err := s.ReadPluginManifestFromFS(ctx, plugin.FS)
|
||||
switch {
|
||||
case errors.Is(err, ErrSignatureTypeUnsigned):
|
||||
s.log.Warn("Plugin is unsigned", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusUnsigned,
|
||||
}, nil
|
||||
case err != nil:
|
||||
s.log.Warn("Plugin signature is invalid", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if !manifest.isV2() {
|
||||
if !manifest.IsV2() {
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
fsFiles, err := plugin.FS.Files()
|
||||
if err != nil {
|
||||
return plugins.Signature{}, fmt.Errorf("files: %w", err)
|
||||
}
|
||||
if len(fsFiles) == 0 {
|
||||
s.log.Warn("No plugin file information in directory", "pluginId", plugin.JSONData.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
@@ -328,7 +337,7 @@ func (s *Signature) validateManifest(ctx context.Context, m PluginManifest, bloc
|
||||
if len(m.Files) == 0 {
|
||||
return invalidFieldErr{field: "files"}
|
||||
}
|
||||
if m.isV2() {
|
||||
if m.IsV2() {
|
||||
if len(m.SignedByOrg) == 0 {
|
||||
return invalidFieldErr{field: "signedByOrg"}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,14 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
)
|
||||
|
||||
func provideDefaultTestService() *Signature {
|
||||
return provideTestServiceWithConfig(&config.PluginManagementCfg{})
|
||||
}
|
||||
|
||||
func provideTestServiceWithConfig(cfg *config.PluginManagementCfg) *Signature {
|
||||
return ProvideService(cfg, statickey.New())
|
||||
}
|
||||
|
||||
func TestReadPluginManifest(t *testing.T) {
|
||||
txt := `-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
@@ -52,7 +60,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
t.Run("valid manifest", func(t *testing.T) {
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
s := provideDefaultTestService()
|
||||
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -68,8 +76,8 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
||||
})
|
||||
|
||||
t.Run("invalid manifest", func(t *testing.T) {
|
||||
s := provideDefaultTestService()
|
||||
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
_, err := s.readPluginManifest(context.Background(), []byte(modified))
|
||||
require.Error(t, err)
|
||||
})
|
||||
@@ -107,7 +115,7 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
t.Run("valid manifest", func(t *testing.T) {
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
s := provideDefaultTestService()
|
||||
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -126,6 +134,12 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
||||
}
|
||||
|
||||
func TestCalculate(t *testing.T) {
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of current dir")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("Validate root URL against App URL for non-private plugin if is specified in manifest", func(t *testing.T) {
|
||||
tcs := []struct {
|
||||
appURL string
|
||||
@@ -147,15 +161,9 @@ func TestCalculate(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of current dir")
|
||||
return
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin")
|
||||
s := ProvideService(&config.PluginManagementCfg{GrafanaAppURL: tc.appURL}, statickey.New())
|
||||
s := provideTestServiceWithConfig(&config.PluginManagementCfg{GrafanaAppURL: tc.appURL})
|
||||
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.ClassExternal
|
||||
@@ -183,7 +191,7 @@ func TestCalculate(t *testing.T) {
|
||||
basePath := "../testdata/renderer-added-file/plugin"
|
||||
|
||||
runningWindows = true
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
s := provideDefaultTestService()
|
||||
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.ClassExternal
|
||||
@@ -247,7 +255,7 @@ func TestCalculate(t *testing.T) {
|
||||
toSlash = tc.platform.toSlashFunc()
|
||||
fromSlash = tc.platform.fromSlashFunc()
|
||||
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
s := provideDefaultTestService()
|
||||
pfs, err := tc.fsFactory()
|
||||
require.NoError(t, err)
|
||||
pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs)
|
||||
@@ -721,7 +729,7 @@ func Test_validateManifest(t *testing.T) {
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
s := provideDefaultTestService()
|
||||
err := s.validateManifest(context.Background(), *tc.manifest, nil)
|
||||
require.Errorf(t, err, tc.expectedErr)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user