[v8.5.x] CLI: Allow relative symlinks in zip archives when installing plugins (#52049)

* resolve conflicts

* fix build issue

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
Will Browne
2022-07-11 16:28:56 +02:00
committed by GitHub
co-authored by Marcus Efraimsson
parent e62f2a7b74
commit 022d7c14d9
14 changed files with 413 additions and 580 deletions
+48 -35
View File
@@ -24,7 +24,6 @@ import (
"time"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util/errutil"
)
type Installer struct {
@@ -80,7 +79,7 @@ func (e ErrVersionNotFound) Error() string {
return fmt.Sprintf("%s v%s either does not exist or is not supported on your system (%s)", e.PluginID, e.RequestedVersion, e.SystemInfo)
}
func New(skipTLSVerify bool, grafanaVersion string, logger Logger) plugins.Installer {
func New(skipTLSVerify bool, grafanaVersion string, logger Logger) *Installer {
return &Installer{
httpClient: makeHttpClient(skipTLSVerify, 10*time.Second),
httpClientNoTimeout: makeHttpClient(skipTLSVerify, 0),
@@ -92,17 +91,8 @@ func New(skipTLSVerify bool, grafanaVersion string, logger Logger) plugins.Insta
// Install downloads the plugin code as a zip file from specified URL
// and then extracts the zip into the provided plugins directory.
func (i *Installer) Install(ctx context.Context, pluginID, version, pluginsDir, pluginZipURL, pluginRepoURL string) error {
isInternal := false
var checksum string
if pluginZipURL == "" {
if strings.HasPrefix(pluginID, "grafana-") {
// At this point the plugin download is going through grafana.com API and thus the name is validated.
// Checking for grafana prefix is how it is done there so no 3rd party plugin should have that prefix.
// You can supply custom plugin name and then set custom download url to 3rd party plugin but then that
// is up to the user to know what she is doing.
isInternal = true
}
plugin, err := i.getPluginMetadataFromPluginRepo(pluginID, pluginRepoURL)
if err != nil {
return err
@@ -137,7 +127,7 @@ func (i *Installer) Install(ctx context.Context, pluginID, version, pluginsDir,
// Create temp file for downloading zip file
tmpFile, err := ioutil.TempFile("", "*.zip")
if err != nil {
return errutil.Wrap("failed to create temporary file", err)
return fmt.Errorf("%v: %w", "failed to create temporary file", err)
}
defer func() {
if err := os.Remove(tmpFile.Name()); err != nil {
@@ -150,16 +140,16 @@ func (i *Installer) Install(ctx context.Context, pluginID, version, pluginsDir,
if err := tmpFile.Close(); err != nil {
i.log.Warn("Failed to close file", "err", err)
}
return errutil.Wrap("failed to download plugin archive", err)
return fmt.Errorf("%v: %w", "failed to download plugin archive", err)
}
err = tmpFile.Close()
if err != nil {
return errutil.Wrap("failed to close tmp file", err)
return fmt.Errorf("%v: %w", "failed to close tmp file", err)
}
err = i.extractFiles(tmpFile.Name(), pluginID, pluginsDir, isInternal)
err = i.extractFiles(tmpFile.Name(), pluginID, pluginsDir)
if err != nil {
return errutil.Wrap("failed to extract plugin archive", err)
return fmt.Errorf("%v: %w", "failed to extract plugin archive", err)
}
res, _ := toPluginDTO(pluginsDir, pluginID)
@@ -170,7 +160,7 @@ func (i *Installer) Install(ctx context.Context, pluginID, version, pluginsDir,
for _, dep := range res.Dependencies.Plugins {
i.log.Infof("Fetching %s dependencies...", res.ID)
if err := i.Install(ctx, dep.ID, normalizeVersion(dep.Version), pluginsDir, "", pluginRepoURL); err != nil {
return errutil.Wrapf(err, "failed to install plugin %s", dep.ID)
return fmt.Errorf("failed to install plugin %s: %w", dep.ID, err)
}
}
@@ -203,7 +193,7 @@ func (i *Installer) DownloadFile(pluginID string, tmpFile *os.File, url string,
// nolint:gosec
f, err := os.Open(url)
if err != nil {
return errutil.Wrap("Failed to read plugin archive", err)
return fmt.Errorf("%v: %w", "Failed to read plugin archive", err)
}
defer func() {
if err := f.Close(); err != nil {
@@ -212,7 +202,7 @@ func (i *Installer) DownloadFile(pluginID string, tmpFile *os.File, url string,
}()
_, err = io.Copy(tmpFile, f)
if err != nil {
return errutil.Wrap("Failed to copy plugin archive", err)
return fmt.Errorf("%v: %w", "Failed to copy plugin archive", err)
}
return nil
}
@@ -260,7 +250,7 @@ func (i *Installer) DownloadFile(pluginID string, tmpFile *os.File, url string,
w := bufio.NewWriter(tmpFile)
h := sha256.New()
if _, err = io.Copy(w, io.TeeReader(bodyReader, h)); err != nil {
return errutil.Wrap("failed to compute SHA256 checksum", err)
return fmt.Errorf("%v: %w", "failed to compute SHA256 checksum", err)
}
if err := w.Flush(); err != nil {
return fmt.Errorf("failed to write to %q: %w", tmpFile.Name(), err)
@@ -509,7 +499,7 @@ func latestSupportedVersion(plugin *Plugin) *Version {
return nil
}
func (i *Installer) extractFiles(archiveFile string, pluginID string, dest string, allowSymlinks bool) error {
func (i *Installer) extractFiles(archiveFile string, pluginID string, dest string) error {
var err error
dest, err = filepath.Abs(dest)
if err != nil {
@@ -571,15 +561,11 @@ func (i *Installer) extractFiles(archiveFile string, pluginID string, dest strin
// We can ignore gosec G304 here since it makes sense to give all users read access
// nolint:gosec
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
return errutil.Wrap("failed to create directory to extract plugin files", err)
return fmt.Errorf("%v: %w", "failed to create directory to extract plugin files", err)
}
if isSymlink(zf) {
if !allowSymlinks {
i.log.Warnf("%v: plugin archive contains a symlink, which is not allowed. Skipping", zf.Name)
continue
}
if err := extractSymlink(zf, dstPath); err != nil {
if err := extractSymlink(existingInstallDir, zf, dstPath); err != nil {
i.log.Warn("failed to extract symlink", "err", err)
continue
}
@@ -587,7 +573,7 @@ func (i *Installer) extractFiles(archiveFile string, pluginID string, dest strin
}
if err := extractFile(zf, dstPath); err != nil {
return errutil.Wrap("failed to extract file", err)
return fmt.Errorf("%v: %w", "failed to extract file", err)
}
}
@@ -598,22 +584,49 @@ func isSymlink(file *zip.File) bool {
return file.Mode()&os.ModeSymlink == os.ModeSymlink
}
func extractSymlink(file *zip.File, filePath string) error {
func extractSymlink(basePath string, file *zip.File, filePath string) error {
// symlink target is the contents of the file
src, err := file.Open()
if err != nil {
return errutil.Wrap("failed to extract file", err)
return fmt.Errorf("%v: %w", "failed to extract file", err)
}
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, src); err != nil {
return errutil.Wrap("failed to copy symlink contents", err)
return fmt.Errorf("%v: %w", "failed to copy symlink contents", err)
}
if err := os.Symlink(strings.TrimSpace(buf.String()), filePath); err != nil {
return errutil.Wrapf(err, "failed to make symbolic link for %v", filePath)
symlinkPath := strings.TrimSpace(buf.String())
if !isSymlinkRelativeTo(basePath, symlinkPath, filePath) {
return fmt.Errorf("symlink %q pointing outside plugin directory is not allowed", filePath)
}
if err := os.Symlink(symlinkPath, filePath); err != nil {
return fmt.Errorf("failed to make symbolic link for %v: %w", filePath, err)
}
return nil
}
// isSymlinkRelativeTo checks whether symlinkDestPath is relative to basePath.
// symlinkOrigPath is the path to file holding the symbolic link.
func isSymlinkRelativeTo(basePath string, symlinkDestPath string, symlinkOrigPath string) bool {
if filepath.IsAbs(symlinkDestPath) {
return false
} else {
fileDir := filepath.Dir(symlinkOrigPath)
cleanPath := filepath.Clean(filepath.Join(fileDir, "/", symlinkDestPath))
p, err := filepath.Rel(basePath, cleanPath)
if err != nil {
return false
}
if strings.HasPrefix(p, ".."+string(filepath.Separator)) {
return false
}
}
return true
}
func extractFile(file *zip.File, filePath string) (err error) {
fileMode := file.Mode()
// This is entry point for backend plugins so we want to make them executable
@@ -636,7 +649,7 @@ func extractFile(file *zip.File, filePath string) (err error) {
return fmt.Errorf("file %q is in use - please stop Grafana, install the plugin and restart Grafana", filePath)
}
return errutil.Wrap("failed to open file", err)
return fmt.Errorf("%v: %w", "failed to open file", err)
}
defer func() {
err = dst.Close()
@@ -644,7 +657,7 @@ func extractFile(file *zip.File, filePath string) (err error) {
src, err := file.Open()
if err != nil {
return errutil.Wrap("failed to extract file", err)
return fmt.Errorf("%v: %w", "failed to extract file", err)
}
defer func() {
err = src.Close()