diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index 0d8315e..6319b5f 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -870,6 +870,30 @@ func TestRunnableBenchmarkScannersFiltersUnsupportedTargetKind(t *testing.T) { } } +func TestUnsafeWindowsShellTargetDetectsInjectionCharacters(t *testing.T) { + unsafe := []string{ + `%PATH%`, + `!DELAYED!`, + `skill" & calc.exe`, + `C:\skills\a"b`, + } + for _, target := range unsafe { + if !unsafeWindowsShellTarget(target) { + t.Errorf("unsafeWindowsShellTarget(%q) = false, want true", target) + } + } + safe := []string{ + `https://example.com/skill`, + `C:\skills\my-skill`, + `./local/skill`, + } + for _, target := range safe { + if unsafeWindowsShellTarget(target) { + t.Errorf("unsafeWindowsShellTarget(%q) = true, want false", target) + } + } +} + func TestValidateRequirementsSkipsScannerResultCredentials(t *testing.T) { opts, err := ParseArgs([]string{ "./my-skill", diff --git a/internal/runner/user_defined_scanner.go b/internal/runner/user_defined_scanner.go index ceef300..962afc8 100644 --- a/internal/runner/user_defined_scanner.go +++ b/internal/runner/user_defined_scanner.go @@ -56,12 +56,26 @@ func (adapter userDefinedScannerAdapter) SupportsTargetKind(kind string) bool { func (adapter userDefinedScannerAdapter) CommandBacked() bool { return true } +// unsafeWindowsShellTarget reports whether a target cannot be interpolated into +// a cmd.exe command line safely: %VAR% expands even inside double quotes, !VAR! +// does too when delayed expansion is enabled, and backslash does not escape " +// for cmd.exe, so a quote in a target could terminate the argument and inject +// host commands. +func unsafeWindowsShellTarget(target string) bool { + return strings.ContainsAny(target, "%\"!") +} + func (adapter userDefinedScannerAdapter) Run(runner ExternalScannerRunner, target string, startedAt string) (ScannerResult, error) { shell := userDefinedScannerShell(runtime.GOOS, runner.SandboxMode) targetReplacement := shell.quote(target) usePositionalTarget := shell.command == "/bin/sh" if usePositionalTarget { targetReplacement = `"$1"` + } else if unsafeWindowsShellTarget(target) { + return ScannerResult{ + Status: "failed", StartedAt: startedAt, CompletedAt: time.Now().UTC().Format(time.RFC3339Nano), + Error: fmt.Sprintf(`User-defined scanner %s cannot receive targets containing %%, !, or " on the Windows host shell; use the Docker sandbox or a target without those characters`, adapter.config.ID), + }, nil } rendered := targetPlaceholderPattern.ReplaceAllStringFunc(adapter.config.Command, func(string) string { return targetReplacement