Просмотр исходного кода

fix(executor): always anchor custom regex patterns and harden shellAfterCompleted
Wrap regex: patterns in a non-capturing group before applying ^...$ so
alternation cannot bypass full-string matching. Return template parse
errors from buildShellAfterCommand, and guard nil binding/action before
building shellAfterCompleted commands.

jamesread 1 неделя назад
Родитель
Сommit
1396184777

+ 1 - 1
service/internal/api/api_test.go

@@ -385,7 +385,7 @@ func testWithEntity(t *testing.T, binding *executor.ActionBinding, rr *Dashboard
 	assert.Equal(t, expectedCanExec, actionResult.CanExec, message)
 }
 
-// buildExecWithoutLogsTestConfig returns config for GHSA-jm28: user "runner" may exec but not read logs.
+// buildExecWithoutLogsTestConfig returns config for GHSA-jm28-2wcr-qf3h: user "runner" may exec but not read logs.
 func buildExecWithoutLogsTestConfig(t *testing.T) (*config.Config, *authpublic.AuthenticatedUser) {
 	t.Helper()
 	cfg := config.DefaultConfig()

+ 0 - 4
service/internal/executor/arguments.go

@@ -315,10 +315,6 @@ func typeSafetyCheckDatetime(value string) error {
 }
 
 func anchorCustomRegexPattern(pattern string) string {
-	if strings.HasPrefix(pattern, "^") && strings.HasSuffix(pattern, "$") {
-		return pattern
-	}
-
 	return "^(?:" + pattern + ")$"
 }
 

+ 7 - 0
service/internal/executor/arguments_test.go

@@ -596,6 +596,13 @@ func TestTypeSafetyCheckRegex(t *testing.T) {
 			value:    "example.com; id",
 			hasError: true,
 		},
+		{
+			name:     "reject alternation bypass when pattern looks anchored",
+			field:    "host",
+			pattern:  "regex:^safe$|bad",
+			value:    "xxxbad",
+			hasError: true,
+		},
 	}
 
 	for _, tt := range tests {

+ 17 - 3
service/internal/executor/executor.go

@@ -1238,8 +1238,22 @@ func stepExecAfter(req *ExecutionRequest) bool {
 	return true
 }
 
-func buildShellAfterCommand(ctx context.Context, req *ExecutionRequest, stdout, stderr *bytes.Buffer) (*exec.Cmd, map[string]string, error) {
+func shellAfterCompletedAction(req *ExecutionRequest) (*config.Action, bool) {
+	if req == nil {
+		return nil, false
+	}
+	if !hasBindingAndAction(req) {
+		return nil, false
+	}
 	if req.Binding.Action.ShellAfterCompleted == "" {
+		return nil, false
+	}
+	return req.Binding.Action, true
+}
+
+func buildShellAfterCommand(ctx context.Context, req *ExecutionRequest, stdout, stderr *bytes.Buffer) (*exec.Cmd, map[string]string, error) {
+	action, ok := shellAfterCompletedAction(req)
+	if !ok {
 		return nil, nil, nil
 	}
 
@@ -1248,14 +1262,14 @@ func buildShellAfterCommand(ctx context.Context, req *ExecutionRequest, stdout,
 		return nil, nil, err
 	}
 
-	finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args)
+	finalParsedCommand, err := tpl.ParseTemplateWithActionContext(action.ShellAfterCompleted, req.Binding.Entity, args)
 	if err != nil {
 		msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
 		req.mutateLogEntry(func(entry *InternalLogEntry) {
 			entry.Output += msg
 		})
 		log.Warn(msg)
-		return nil, nil, nil
+		return nil, nil, err
 	}
 
 	cmd := wrapCommandInShell(ctx, finalParsedCommand)