Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pkg/workflow/features_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment on lines +229 to +239
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test name implies bash tool configuration affects validation, but validateFeatures() currently only validates features.action-tag and does not inspect ParsedTools. Consider renaming the test (or adding a comment) to avoid implying a dependency that doesn’t exist.

Copilot uses AI. Check for mistakes.
},
}

for _, tt := range tests {
Expand Down
31 changes: 31 additions & 0 deletions pkg/workflow/strict_mode_permissions_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != ""
}
Comment on lines +84 to +99
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateStrictDisableXPIA() only checks for the exact key disable-xpia-prompt. Feature lookup elsewhere is case-insensitive (see pkg/workflow/features.go:50-56), and the schema allows arbitrary feature keys, so features: { Disable-XPIA-Prompt: true } would still disable the XPIA prompt but bypass this strict-mode check. Consider normalizing keys (e.g., strings.ToLower on feature names) or reusing the existing case-insensitive feature parsing logic so strict mode can’t be bypassed via casing.

Copilot uses AI. Check for mistakes.
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.
Expand Down
8 changes: 8 additions & 0 deletions pkg/workflow/strict_mode_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
}
Comment on lines +87 to +92
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This strict-mode check is wired into validateStrictMode(), but validateStrictMode() runs before imports are processed/merged (compiler_orchestrator_engine.go calls validateStrictMode before ProcessImportsFromFrontmatterWithSource, and features are merged later in compiler_orchestrator_workflow.go:178-185). As a result, disable-xpia-prompt coming from an import won’t be rejected in strict mode. To enforce the stated security guarantee, run this validation after imports/features are merged (or validate against the merged feature set).

Copilot uses AI. Check for mistakes.

strictModeValidationLog.Printf("Strict mode validation completed: error_count=%d", collector.Count())

return collector.FormattedError("strict mode")
Expand Down
110 changes: 110 additions & 0 deletions pkg/workflow/strict_mode_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
})
}
}
Loading