diff --git a/pkg/workflow/features_validation_test.go b/pkg/workflow/features_validation_test.go index 633ed11d48..42a77fe660 100644 --- a/pkg/workflow/features_validation_test.go +++ b/pkg/workflow/features_validation_test.go @@ -226,6 +226,18 @@ func TestValidateFeatures(t *testing.T) { }, expectError: false, }, + { + name: "disable-xpia-prompt with bash tool - allowed in non-strict mode", + data: &WorkflowData{ + Features: map[string]any{ + "disable-xpia-prompt": true, + }, + ParsedTools: NewTools(map[string]any{ + "bash": true, + }), + }, + expectError: false, + }, } for _, tt := range tests { diff --git a/pkg/workflow/strict_mode_permissions_validation.go b/pkg/workflow/strict_mode_permissions_validation.go index edd38ea0fa..aea44f1144 100644 --- a/pkg/workflow/strict_mode_permissions_validation.go +++ b/pkg/workflow/strict_mode_permissions_validation.go @@ -73,6 +73,37 @@ func (c *Compiler) validateStrictDeprecatedFields(frontmatter map[string]any) er return nil } +// validateStrictDisableXPIA refuses use of the disable-xpia-prompt feature flag in strict mode. +// Disabling XPIA (Cross-Prompt Injection Attack) protection removes the primary defense against +// prompt-injection attacks in production workflows. +func (c *Compiler) validateStrictDisableXPIA(frontmatter map[string]any) error { + featuresValue, exists := frontmatter["features"] + if !exists { + return nil + } + featuresMap, ok := featuresValue.(map[string]any) + if !ok { + return nil + } + flagVal, exists := featuresMap["disable-xpia-prompt"] + if !exists { + return nil + } + // Only reject when the flag is explicitly enabled (true / non-empty string) + enabled := false + switch v := flagVal.(type) { + case bool: + enabled = v + case string: + enabled = v != "" + } + if !enabled { + return nil + } + strictModeValidationLog.Printf("disable-xpia-prompt validation failed: feature flag enabled in strict mode") + return errors.New("strict mode: 'disable-xpia-prompt: true' is not allowed because it removes XPIA (Cross-Prompt Injection Attack) protection from the workflow. This eliminates the primary defense against prompt-injection attacks. Remove the disable-xpia-prompt feature flag or set 'strict: false' to disable strict mode") +} + // validateStrictFirewall requires firewall to be enabled in strict mode for copilot and codex engines // when network domains are provided (non-wildcard). // In strict mode, ALL engines (regardless of LLM gateway support) disallow sandbox.agent: false. diff --git a/pkg/workflow/strict_mode_validation.go b/pkg/workflow/strict_mode_validation.go index 92d389078f..a19824dfc4 100644 --- a/pkg/workflow/strict_mode_validation.go +++ b/pkg/workflow/strict_mode_validation.go @@ -30,6 +30,7 @@ var strictModeValidationLog = newValidationLogger("strict_mode") // 3. validateStrictMCPNetwork() - Requires top-level network config for container-based MCP servers // 4. validateStrictTools() - Validates tools configuration (e.g., serena local mode) // 5. validateStrictDeprecatedFields() - Refuses deprecated fields +// 6. validateStrictDisableXPIA() - Refuses disable-xpia-prompt feature flag // // Note: Env secrets validation (validateEnvSecrets) is called separately outside of strict mode // to emit warnings in non-strict mode and errors in strict mode. @@ -83,6 +84,13 @@ func (c *Compiler) validateStrictMode(frontmatter map[string]any, networkPermiss } } + // 6. Refuse disable-xpia-prompt feature flag + if err := c.validateStrictDisableXPIA(frontmatter); err != nil { + if returnErr := collector.Add(err); returnErr != nil { + return returnErr // Fail-fast mode + } + } + strictModeValidationLog.Printf("Strict mode validation completed: error_count=%d", collector.Count()) return collector.FormattedError("strict mode") diff --git a/pkg/workflow/strict_mode_validation_test.go b/pkg/workflow/strict_mode_validation_test.go index ebc28607b2..892792a3a3 100644 --- a/pkg/workflow/strict_mode_validation_test.go +++ b/pkg/workflow/strict_mode_validation_test.go @@ -662,3 +662,113 @@ func TestValidateStrictCacheMemoryScope(t *testing.T) { }) } } + +// TestValidateStrictDisableXPIA tests the validateStrictDisableXPIA function +func TestValidateStrictDisableXPIA(t *testing.T) { + tests := []struct { + name string + frontmatter map[string]any + expectError bool + errorMsg string + }{ + { + name: "no features field - allowed", + frontmatter: map[string]any{"on": "push"}, + expectError: false, + }, + { + name: "features without disable-xpia-prompt - allowed", + frontmatter: map[string]any{ + "on": "push", + "features": map[string]any{ + "action-tag": "v0", + }, + }, + expectError: false, + }, + { + name: "disable-xpia-prompt: false - allowed", + frontmatter: map[string]any{ + "on": "push", + "features": map[string]any{ + "disable-xpia-prompt": false, + }, + }, + expectError: false, + }, + { + name: "disable-xpia-prompt: true - rejected", + frontmatter: map[string]any{ + "on": "push", + "features": map[string]any{ + "disable-xpia-prompt": true, + }, + }, + expectError: true, + errorMsg: "strict mode: 'disable-xpia-prompt: true' is not allowed", + }, + { + name: "disable-xpia-prompt: true with bash tool - rejected (bash state irrelevant)", + frontmatter: map[string]any{ + "on": "push", + "features": map[string]any{ + "disable-xpia-prompt": true, + }, + "tools": map[string]any{ + "bash": true, + }, + }, + expectError: true, + errorMsg: "strict mode: 'disable-xpia-prompt: true' is not allowed", + }, + { + name: "disable-xpia-prompt: true without bash tool - still rejected", + frontmatter: map[string]any{ + "on": "push", + "features": map[string]any{ + "disable-xpia-prompt": true, + }, + }, + expectError: true, + errorMsg: "strict mode: 'disable-xpia-prompt: true' is not allowed", + }, + { + name: "disable-xpia-prompt as non-empty string - rejected", + frontmatter: map[string]any{ + "on": "push", + "features": map[string]any{ + "disable-xpia-prompt": "yes", + }, + }, + expectError: true, + errorMsg: "strict mode: 'disable-xpia-prompt: true' is not allowed", + }, + { + name: "disable-xpia-prompt as empty string - allowed", + frontmatter: map[string]any{ + "on": "push", + "features": map[string]any{ + "disable-xpia-prompt": "", + }, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compiler := NewCompiler() + err := compiler.validateStrictDisableXPIA(tt.frontmatter) + + if tt.expectError && err == nil { + t.Error("Expected validation to fail but it succeeded") + } else if !tt.expectError && err != nil { + t.Errorf("Expected validation to succeed but it failed: %v", err) + } else if tt.expectError && err != nil && tt.errorMsg != "" { + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error containing '%s', got '%s'", tt.errorMsg, err.Error()) + } + } + }) + } +}