Skip to content

security: reject disable-xpia-prompt in strict mode at compile time#28057

Merged
pelikhan merged 4 commits intomainfrom
copilot/disable-xpia-prompt-guard
Apr 23, 2026
Merged

security: reject disable-xpia-prompt in strict mode at compile time#28057
pelikhan merged 4 commits intomainfrom
copilot/disable-xpia-prompt-guard

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 23, 2026

disable-xpia-prompt: true in a strict-mode workflow removes the primary XPIA (Cross-Prompt Injection Attack) defense. In strict mode this combination is now rejected at compile time regardless of bash tool state.

Changes

  • New strict mode validation (strict_mode_permissions_validation.go): validateStrictDisableXPIA() rejects any workflow that sets disable-xpia-prompt: true when compiled in strict mode. Bash tool configuration is not considered — the flag alone is sufficient to fail validation.
  • Wired into strict mode orchestrator (strict_mode_validation.go): Added as step 6 in validateStrictMode().
  • Non-strict mode unaffected: disable-xpia-prompt: true is still permitted outside strict mode, avoiding breakage for test and development workflows.
  • Tests (strict_mode_validation_test.go): covers all relevant cases — flag enabled/disabled, with/without bash tool, string values.
features:
  disable-xpia-prompt: true

→ strict mode compile error:

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

Copilot AI changed the title [WIP] Fix disable-xpia-prompt vulnerability in workflows security: reject disable-xpia-prompt combined with bash tool access at compile time Apr 23, 2026
Copilot AI requested a review from szabta89 April 23, 2026 12:27
@pelikhan
Copy link
Copy Markdown
Collaborator

@szabta89 wouldn't this break testing? Proper fix would be to make it an error in strict mode.

@szabta89
Copy link
Copy Markdown
Contributor

@szabta89 wouldn't this break testing? Proper fix would be to make it an error in strict mode.

You are right. Either only add this in strict mode, or, even better, just close this as a non-issue with a comment. I have updated the pentester workflows now to consider closed security findings when making a decision about potentially new findings, so, hopefully, we will not produce repeat findings.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot mark this feature as error in strict mode and ignore bash state

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 23, 2026

@copilot mark this feature as error in strict mode and ignore bash state

Done in e4c68ec. disable-xpia-prompt: true is now a strict mode error only (via validateStrictDisableXPIA()), regardless of bash tool state. Non-strict mode workflows are unaffected.

Copilot AI changed the title security: reject disable-xpia-prompt combined with bash tool access at compile time security: reject disable-xpia-prompt in strict mode at compile time Apr 23, 2026
Copilot AI requested a review from pelikhan April 23, 2026 13:18
@pelikhan pelikhan marked this pull request as ready for review April 23, 2026 13:19
Copilot AI review requested due to automatic review settings April 23, 2026 13:19
@pelikhan pelikhan merged commit 1b22aed into main Apr 23, 2026
16 of 19 checks passed
@pelikhan pelikhan deleted the copilot/disable-xpia-prompt-guard branch April 23, 2026 13:19
@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 80/100

Excellent test quality

Metric Value
New/modified tests analyzed 2 (1 new function + 1 new table row)
✅ Design tests (behavioral contracts) 2 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (100%)
Duplicate test clusters 0
Test inflation detected ⚠️ Yes (acceptable — see below)
🚨 Coding-guideline violations None

Test Classification Details

Test File Classification Issues Detected
TestValidateStrictDisableXPIA pkg/workflow/strict_mode_validation_test.go ✅ Design None — comprehensive table-driven test with error & edge cases
New table row: "disable-xpia-prompt with bash tool - allowed in non-strict mode" pkg/workflow/features_validation_test.go ✅ Design None — verifies non-strict mode permits the flag

Analysis

TestValidateStrictDisableXPIA is a well-structured table-driven test covering the full behavioral contract of the new security check. The 8 sub-cases exercise:

  • Happy-path absence (no features field, no disable-xpia-prompt key) ✅
  • Explicit false value allowed ✅
  • Explicit true value rejected ✅
  • true with/without bash tool — both rejected, confirming tool state is irrelevant ✅
  • Non-empty string treated as truthy and rejected ✅
  • Empty string treated as falsy and allowed ✅

features_validation_test.go addition confirms that non-strict mode still permits disable-xpia-prompt: true when the bash tool is present, covering the complementary non-strict code path.

No mock libraries, no testify mocks, no missing build tags. Both files correctly carry //go:build !integration.

⚠️ Test Inflation Note (informational, not a blocking issue)

strict_mode_validation_test.go added 110 lines against strict_mode_validation.go's 8 new lines (ratio ≈ 13.75:1), triggering the mechanical inflation flag. However, this is the expected pattern for a focused security invariant: the dispatch logic is small, while comprehensive edge-case coverage is large. The production validation logic lives primarily in strict_mode_permissions_validation.go (+31 lines). Taking both production files together the effective ratio is roughly 122 test lines to 39 production lines (~3:1) — only modestly above threshold and entirely justified for a security-critical check.

Minor style note: the test runner uses stdlib assertions (t.Error, t.Errorf) rather than testify. That is fine per project guidelines, but assertion messages could be slightly more descriptive (e.g., t.Errorf("validateStrictDisableXPIA(%q): expected error, got nil", tt.name)). This is not a blocking issue.


Language Support

  • 🐹 Go (*_test.go): 2 test units analyzed — unit (//go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests changed

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). The tests enforce genuine behavioral contracts: strict-mode rejection of disable-xpia-prompt: true and non-strict-mode allowance. No coding-guideline violations detected.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References: §24837564064

🧪 Test quality analysis by Test Quality Sentinel · ● 594.1K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 80/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). The new TestValidateStrictDisableXPIA covers all meaningful behavioral edge cases for the security invariant.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds strict-mode compile-time validation to prevent workflows from disabling the XPIA (Cross-Prompt Injection Attack) defense via features.disable-xpia-prompt: true.

Changes:

  • Added validateStrictDisableXPIA() strict-mode validator to reject disable-xpia-prompt when enabled.
  • Wired the new validator into the strict mode validation orchestrator.
  • Added/updated unit tests to cover strict-mode behavior and to confirm non-strict mode remains permissive.
Show a summary per file
File Description
pkg/workflow/strict_mode_permissions_validation.go Adds the new strict-mode validation function that rejects disabling XPIA.
pkg/workflow/strict_mode_validation.go Calls the new strict-mode validation as part of the strict mode validation sequence.
pkg/workflow/strict_mode_validation_test.go Adds unit tests for the new strict-mode validation behavior.
pkg/workflow/features_validation_test.go Adds a non-strict-mode test asserting the feature flag remains allowed.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (1)

pkg/workflow/strict_mode_validation_test.go:775

  • The new tests cover validateStrictDisableXPIA() directly, but they don’t assert that validateStrictMode() (the orchestrator) rejects workflows with disable-xpia-prompt enabled. Adding at least one case to TestValidateStrictMode for this flag (and a mixed-case key variant, given case-insensitive feature handling) would prevent regressions in the wiring and bypasses.
// 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())
				}
			}
		})
	}
}

  • Files reviewed: 4/4 changed files
  • Comments generated: 3

Comment on lines +84 to +99
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 != ""
}
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.
Comment on lines +87 to +92
// 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
}
}
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.
Comment on lines +229 to +239
{
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,
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants