Skip to content

Commit 1b22aed

Browse files
authored
security: reject disable-xpia-prompt in strict mode at compile time (#28057)
1 parent a5c3c3b commit 1b22aed

4 files changed

Lines changed: 161 additions & 0 deletions

File tree

pkg/workflow/features_validation_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,18 @@ func TestValidateFeatures(t *testing.T) {
226226
},
227227
expectError: false,
228228
},
229+
{
230+
name: "disable-xpia-prompt with bash tool - allowed in non-strict mode",
231+
data: &WorkflowData{
232+
Features: map[string]any{
233+
"disable-xpia-prompt": true,
234+
},
235+
ParsedTools: NewTools(map[string]any{
236+
"bash": true,
237+
}),
238+
},
239+
expectError: false,
240+
},
229241
}
230242

231243
for _, tt := range tests {

pkg/workflow/strict_mode_permissions_validation.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,37 @@ func (c *Compiler) validateStrictDeprecatedFields(frontmatter map[string]any) er
7373
return nil
7474
}
7575

76+
// validateStrictDisableXPIA refuses use of the disable-xpia-prompt feature flag in strict mode.
77+
// Disabling XPIA (Cross-Prompt Injection Attack) protection removes the primary defense against
78+
// prompt-injection attacks in production workflows.
79+
func (c *Compiler) validateStrictDisableXPIA(frontmatter map[string]any) error {
80+
featuresValue, exists := frontmatter["features"]
81+
if !exists {
82+
return nil
83+
}
84+
featuresMap, ok := featuresValue.(map[string]any)
85+
if !ok {
86+
return nil
87+
}
88+
flagVal, exists := featuresMap["disable-xpia-prompt"]
89+
if !exists {
90+
return nil
91+
}
92+
// Only reject when the flag is explicitly enabled (true / non-empty string)
93+
enabled := false
94+
switch v := flagVal.(type) {
95+
case bool:
96+
enabled = v
97+
case string:
98+
enabled = v != ""
99+
}
100+
if !enabled {
101+
return nil
102+
}
103+
strictModeValidationLog.Printf("disable-xpia-prompt validation failed: feature flag enabled in strict mode")
104+
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")
105+
}
106+
76107
// validateStrictFirewall requires firewall to be enabled in strict mode for copilot and codex engines
77108
// when network domains are provided (non-wildcard).
78109
// In strict mode, ALL engines (regardless of LLM gateway support) disallow sandbox.agent: false.

pkg/workflow/strict_mode_validation.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ var strictModeValidationLog = newValidationLogger("strict_mode")
3030
// 3. validateStrictMCPNetwork() - Requires top-level network config for container-based MCP servers
3131
// 4. validateStrictTools() - Validates tools configuration (e.g., serena local mode)
3232
// 5. validateStrictDeprecatedFields() - Refuses deprecated fields
33+
// 6. validateStrictDisableXPIA() - Refuses disable-xpia-prompt feature flag
3334
//
3435
// Note: Env secrets validation (validateEnvSecrets) is called separately outside of strict mode
3536
// 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
8384
}
8485
}
8586

87+
// 6. Refuse disable-xpia-prompt feature flag
88+
if err := c.validateStrictDisableXPIA(frontmatter); err != nil {
89+
if returnErr := collector.Add(err); returnErr != nil {
90+
return returnErr // Fail-fast mode
91+
}
92+
}
93+
8694
strictModeValidationLog.Printf("Strict mode validation completed: error_count=%d", collector.Count())
8795

8896
return collector.FormattedError("strict mode")

pkg/workflow/strict_mode_validation_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,3 +662,113 @@ func TestValidateStrictCacheMemoryScope(t *testing.T) {
662662
})
663663
}
664664
}
665+
666+
// TestValidateStrictDisableXPIA tests the validateStrictDisableXPIA function
667+
func TestValidateStrictDisableXPIA(t *testing.T) {
668+
tests := []struct {
669+
name string
670+
frontmatter map[string]any
671+
expectError bool
672+
errorMsg string
673+
}{
674+
{
675+
name: "no features field - allowed",
676+
frontmatter: map[string]any{"on": "push"},
677+
expectError: false,
678+
},
679+
{
680+
name: "features without disable-xpia-prompt - allowed",
681+
frontmatter: map[string]any{
682+
"on": "push",
683+
"features": map[string]any{
684+
"action-tag": "v0",
685+
},
686+
},
687+
expectError: false,
688+
},
689+
{
690+
name: "disable-xpia-prompt: false - allowed",
691+
frontmatter: map[string]any{
692+
"on": "push",
693+
"features": map[string]any{
694+
"disable-xpia-prompt": false,
695+
},
696+
},
697+
expectError: false,
698+
},
699+
{
700+
name: "disable-xpia-prompt: true - rejected",
701+
frontmatter: map[string]any{
702+
"on": "push",
703+
"features": map[string]any{
704+
"disable-xpia-prompt": true,
705+
},
706+
},
707+
expectError: true,
708+
errorMsg: "strict mode: 'disable-xpia-prompt: true' is not allowed",
709+
},
710+
{
711+
name: "disable-xpia-prompt: true with bash tool - rejected (bash state irrelevant)",
712+
frontmatter: map[string]any{
713+
"on": "push",
714+
"features": map[string]any{
715+
"disable-xpia-prompt": true,
716+
},
717+
"tools": map[string]any{
718+
"bash": true,
719+
},
720+
},
721+
expectError: true,
722+
errorMsg: "strict mode: 'disable-xpia-prompt: true' is not allowed",
723+
},
724+
{
725+
name: "disable-xpia-prompt: true without bash tool - still rejected",
726+
frontmatter: map[string]any{
727+
"on": "push",
728+
"features": map[string]any{
729+
"disable-xpia-prompt": true,
730+
},
731+
},
732+
expectError: true,
733+
errorMsg: "strict mode: 'disable-xpia-prompt: true' is not allowed",
734+
},
735+
{
736+
name: "disable-xpia-prompt as non-empty string - rejected",
737+
frontmatter: map[string]any{
738+
"on": "push",
739+
"features": map[string]any{
740+
"disable-xpia-prompt": "yes",
741+
},
742+
},
743+
expectError: true,
744+
errorMsg: "strict mode: 'disable-xpia-prompt: true' is not allowed",
745+
},
746+
{
747+
name: "disable-xpia-prompt as empty string - allowed",
748+
frontmatter: map[string]any{
749+
"on": "push",
750+
"features": map[string]any{
751+
"disable-xpia-prompt": "",
752+
},
753+
},
754+
expectError: false,
755+
},
756+
}
757+
758+
for _, tt := range tests {
759+
t.Run(tt.name, func(t *testing.T) {
760+
compiler := NewCompiler()
761+
err := compiler.validateStrictDisableXPIA(tt.frontmatter)
762+
763+
if tt.expectError && err == nil {
764+
t.Error("Expected validation to fail but it succeeded")
765+
} else if !tt.expectError && err != nil {
766+
t.Errorf("Expected validation to succeed but it failed: %v", err)
767+
} else if tt.expectError && err != nil && tt.errorMsg != "" {
768+
if !strings.Contains(err.Error(), tt.errorMsg) {
769+
t.Errorf("Expected error containing '%s', got '%s'", tt.errorMsg, err.Error())
770+
}
771+
}
772+
})
773+
}
774+
}

0 commit comments

Comments
 (0)