Harden marketplace install inputs
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Snider 2026-04-15 23:07:20 +01:00
parent d54a57e158
commit b87a0da3ba
3 changed files with 68 additions and 5 deletions

View file

@ -124,7 +124,9 @@ func TestMarketplace_registerMarketplaceActions_Good(t *testing.T) {
installed, ok := installResult.Value.(map[string]any)
require.True(t, ok)
assert.Equal(t, installDir, installed["install_dir"])
assert.Equal(t, filepath.Join(installDir, "core-ui"), installed["target_dir"])
resolvedInstallDir, err := filepath.EvalSymlinks(installDir)
require.NoError(t, err)
assert.Equal(t, filepath.Join(resolvedInstallDir, "core-ui"), installed["target_dir"])
contents, err := os.ReadFile(gitLog)
require.NoError(t, err)

View file

@ -11,6 +11,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
@ -130,7 +131,7 @@ func (i Installer) Install(ctx context.Context, manifest Manifest) (string, erro
if err := validateManifestName(manifest.Name); err != nil {
return "", err
}
if err := validateCloneArg("repository", manifest.Repository); err != nil {
if err := validateRepositorySource(manifest.Repository); err != nil {
return "", err
}
if err := validateCloneArgOptional("ref", manifest.Ref); err != nil {
@ -139,16 +140,20 @@ func (i Installer) Install(ctx context.Context, manifest Manifest) (string, erro
if err := os.MkdirAll(i.InstallDir, 0o755); err != nil {
return "", err
}
targetDir := filepath.Join(i.InstallDir, safeName(manifest.Name))
rootAbs, err := filepath.Abs(i.InstallDir)
if err != nil {
return "", err
}
rootResolved, err := filepath.EvalSymlinks(rootAbs)
if err != nil {
return "", err
}
targetDir := filepath.Join(rootResolved, safeName(manifest.Name))
targetAbs, err := filepath.Abs(targetDir)
if err != nil {
return "", err
}
rel, err := filepath.Rel(rootAbs, targetAbs)
rel, err := filepath.Rel(rootResolved, targetAbs)
if err != nil {
return "", err
}
@ -238,6 +243,41 @@ func validateCloneArgOptional(label, value string) error {
return validateCloneArg(label, trimmed)
}
func validateRepositorySource(value string) error {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return errors.New("repository is required")
}
if strings.ContainsAny(trimmed, "\x00\r\n") {
return errors.New("repository contains invalid control characters")
}
if strings.HasPrefix(strings.ToLower(trimmed), "ext::") {
return errors.New("repository must not use git remote helper protocols")
}
if strings.HasPrefix(trimmed, "-") {
return errors.New("repository must not begin with a dash")
}
if strings.Contains(trimmed, "://") {
parsed, err := url.Parse(trimmed)
if err != nil {
return err
}
switch strings.ToLower(parsed.Scheme) {
case "http", "https", "ssh", "git":
default:
return fmt.Errorf("repository scheme %q is not allowed", parsed.Scheme)
}
return nil
}
if strings.Contains(trimmed, string(filepath.Separator)) || filepath.IsAbs(trimmed) {
return errors.New("repository path clones are not allowed")
}
if !strings.Contains(trimmed, ":") {
return errors.New("repository must be a URL or scp-style remote")
}
return nil
}
func DigestManifest(manifest Manifest) string {
hash := sha256.Sum256([]byte(manifest.Name + ":" + manifest.Version + ":" + manifest.Repository + ":" + manifest.Ref))
return hex.EncodeToString(hash[:])

View file

@ -185,7 +185,9 @@ func TestMarketplace_Install_Good(t *testing.T) {
Ref: "main",
}))
require.NoError(t, err)
assert.Equal(t, filepath.Join(targetRoot, "core-ui"), targetDir)
resolvedRoot, err := filepath.EvalSymlinks(targetRoot)
require.NoError(t, err)
assert.Equal(t, filepath.Join(resolvedRoot, "core-ui"), targetDir)
_, err = os.Stat(targetDir)
require.NoError(t, err)
@ -366,6 +368,25 @@ func TestMarketplace_validateCloneArg_Ugly(t *testing.T) {
assert.Contains(t, err.Error(), "invalid control characters")
}
func TestMarketplace_validateRepositorySource_Good(t *testing.T) {
require.NoError(t, validateRepositorySource("https://example.com/core-ui.git"))
require.NoError(t, validateRepositorySource("git@example.com:core-ui.git"))
}
func TestMarketplace_validateRepositorySource_Bad(t *testing.T) {
err := validateRepositorySource("ext::sh -c id")
require.Error(t, err)
assert.Contains(t, err.Error(), "remote helper")
}
func TestMarketplace_validateRepositorySource_Ugly(t *testing.T) {
err := validateRepositorySource("file:///tmp/core-ui.git")
require.Error(t, err)
assert.Contains(t, err.Error(), "not allowed")
}
func TestMarketplace_decodeManifestList_Good(t *testing.T) {
manifests, err := decodeManifestList([]byte(`[{"name":"core-ui","version":"1.2.3"},{"name":"core-chat","version":"0.9.0"}]`))