Skip to content

fix: harden isSafeExpression — refuse literal operands in &&/|| and fix spec-enforcer expression#29936

Merged
pelikhan merged 5 commits intomainfrom
copilot/fix-package-specification-enforcer
May 3, 2026
Merged

fix: harden isSafeExpression — refuse literal operands in &&/|| and fix spec-enforcer expression#29936
pelikhan merged 5 commits intomainfrom
copilot/fix-package-specification-enforcer

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 3, 2026

spec-enforcer.md runtime-imports itself during the activation job. The JS runtime validator (isSafeExpression) rejected compound expressions, causing immediate activation failure:

ERR_API: ERR_VALIDATION: File workflows/spec-enforcer.md contains unauthorized GitHub Actions expressions

Changes

  • Standalone literals — string/number/boolean values are now valid as complete standalone expressions in isSafeExpression
  • AND (&&) compound expressions — both sides must be safe non-literal property expressions; a bare literal on either side of && is refused (a literal in a conjunction is semantically incomplete and may hide injection vectors)
  • Literal on left of || refused'literal' || expr is rejected (always-truthy left side makes the right side dead code); the established OR-fallback pattern expr || 'literal' remains valid
  • Comparison expressions (==, !=, <, >, <=, >=) — LHS property is validated against the allowlist; comparison check only runs for leaf expressions with no top-level || / &&
  • Tightened string-literal regex — changed from /^(['"\]).\1$/to/^'(?:[^'\\]|\.)'$|.../, preventing compound expressions like 'a' || secrets.TOKEN || 'b'` from being misclassified as a single string literal
  • Fall-through security fix — OR and AND blocks always return early; previously an unsafe RHS could slip through to the comparison check (e.g. github.actor == 'value' || secrets.TOKEN was incorrectly allowed)
  • spec-enforcer.md updated — the ternary-style expression ${{ condition && 'full-sweep' || 'round-robin' }} (which uses a literal in &&) is replaced with the valid OR-fallback form: ${{ github.event.inputs.enforce_all || 'round-robin' }}

Tests

  • fuzz_is_safe_expression_harness.cjs — new Go-driveable fuzz harness for isSafeExpression callable via stdin JSON
  • fuzz_is_safe_expression_harness.test.cjs — 295 property-based fuzz tests covering:
    • Standalone literal allow/block (including escape-sequence and nested-expression rejection)
    • Safe-leaf × all 6 comparison operators exhaustive matrix
    • AND expressions: safe×safe without literals (allowed); literal on either side (blocked); unsafe on either side (blocked)
    • OR expressions: literal on left side (blocked); unsafe right side (blocked); OR-fallback expr || 'literal' (allowed)
    • Ternary-style cond && 'val' || 'default' — all refused (literal in AND operand)
    • Security invariant sweep: 9 wrapper forms × 6 unsafe leaf types — no wrapping can make secrets.*, vars.*, or runner.* safe
    • Dangerous prototype-pollution property names in compound expressions
  • Go fuzz tests updated (pkg/parser/runtime_import_fuzz_test.go) — expanded seed corpus with compound expression patterns including refused forms; strengthened invariant checks
  • Go unit tests updated (pkg/parser/runtime_import_test.go) — table-driven cases covering standalone literals, comparisons, AND without literals, refused AND-with-literal, refused ternary, OR-fallback allowed, literal-on-OR-left refused, and unsafe compound variants

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented May 3, 2026

@copilot add more tests, fuzz tests

…ix spec-enforcer validation

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/ec9c665d-7ab1-42fc-a7c9-e64ba729ace7

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix package specification enforcer validation error fix: support AND expressions and comparisons in isSafeExpression to unblock spec-enforcer May 3, 2026
Copilot AI requested a review from pelikhan May 3, 2026 15:29
…compound expressions

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/176a13d4-89c8-425e-ada5-3dac2bdf2132

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 3, 2026

@copilot add more tests, fuzz tests

Added in commit 92b0b65:

New JS fuzz harness (fuzz_is_safe_expression_harness.cjs) — callable from Go's fuzzing framework via stdin JSON, plus a test file (fuzz_is_safe_expression_harness.test.cjs) with 275 property-based tests covering:

  • Standalone literal allow/block
  • Safe-leaf × all-comparison-operators matrix (6 operators × 11 safe leaves = 66 cases)
  • AND: safe×safe pairs (allowed) + every unsafe leaf on both sides (blocked)
  • OR: unsafe right side blocked for all unsafe leaves
  • Ternary-style AND/OR chains: allowed and blocked permutations
  • Security invariant sweep: every wrapper form of secrets.*, vars.*, runner.* is blocked (11 wrapper patterns × 6 unsafe leaves = 66 invariant checks)
  • Dangerous prototype-pollution property names in compound expressions

Go fuzz tests updated (pkg/parser/runtime_import_fuzz_test.go): expanded seed corpus for both FuzzRuntimeImportExpressionValidation and FuzzRuntimeImportProcessExpressions with compound expression patterns, and strengthened the FuzzRuntimeImportExpressionValidation invariant to also flag compound expressions whose leading token is an unsafe namespace.

Go unit tests updated (pkg/parser/runtime_import_test.go): added 8 new table-driven cases in TestRuntimeImportExpressionValidation covering standalone literals, comparisons, AND, ternary patterns, and the corresponding unsafe variants.

@pelikhan pelikhan marked this pull request as ready for review May 3, 2026 15:39
Copilot AI review requested due to automatic review settings May 3, 2026 15:39
@github-actions github-actions Bot mentioned this pull request May 3, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 3, 2026

🧪 Test Quality Sentinel Report

Test Quality Score: 80/100

Excellent — all new tests enforce behavioral contracts.

Metric Value
New/modified tests analyzed ~50 (sampling applied; 250+ auto-generated via loops)
✅ Design tests (behavioral contracts) 100%
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 100%
Duplicate test clusters 0
Test inflation detected Yes — fuzz_is_safe_expression_harness.test.cjs (263 lines test vs 70 lines production = 3.75:1)
🚨 Coding-guideline violations None

⚠️ Sampling applied — analyzed the first 50 of ~250+ generated test cases. Prioritized newly added describe/it blocks and table rows.


Test Classification Details

View all test groups (11 groups across 4 files)
Test / Group File Classification Notes
standalone literals (safe) fuzz_is_safe_expression_harness.test.cjs ✅ Design 8 safe-literal cases + 2 rejection checks
standalone literals (rejected w/ injections) fuzz_is_safe_expression_harness.test.cjs ✅ Design Hex/unicode escape injection blocked
comparisons allowed (safe leaves × all 6 ops) fuzz_is_safe_expression_harness.test.cjs ✅ Design 66 auto-generated cases
comparisons blocked (unsafe left side) fuzz_is_safe_expression_harness.test.cjs ✅ Design All 5 unsafe namespaces checked
AND safe×safe fuzz_is_safe_expression_harness.test.cjs ✅ Design Combinatorial; spot-checked representative pairs
AND unsafe side blocked fuzz_is_safe_expression_harness.test.cjs ✅ Design Left and right unsafe each checked
OR unsafe blocked fuzz_is_safe_expression_harness.test.cjs ✅ Design Includes comparison-then-OR case
ternary-style AND/OR (allowed + blocked) fuzz_is_safe_expression_harness.test.cjs ✅ Design Includes the exact spec-enforcer pattern
security invariants matrix (5 unsafe × 11 wrappers) fuzz_is_safe_expression_harness.test.cjs ✅ Design 55 expressions + dangerous property name checks
exhaustive safe-leaf × operator matrix fuzz_is_safe_expression_harness.test.cjs ✅ Design 66 cases covering all leaf/op combos
12 new it() blocks for all compound forms runtime_import.test.cjs ✅ Design Mirrors harness at a coarser level; good integration check
8 new table rows in TestRuntimeImportExpressionValidation pkg/parser/runtime_import_test.go ✅ Design 3 safe + 5 unsafe; all use descriptive name + description fields
Fuzz seed corpus extensions + leading-unsafe invariant pkg/parser/runtime_import_fuzz_test.go ✅ Design Seed corpus covers AND/OR/ternary + unsafe boundaries; new invariant check with t.Errorf message

Flagged Tests — Requires Review

No tests were flagged for behavioral quality issues. One structural note:

⚠️ Test inflation — fuzz_is_safe_expression_harness.test.cjs (263 lines) vs fuzz_is_safe_expression_harness.cjs (70 lines)

Classification: Structural / metric flag only — not a quality failure.

Context: Property-based / fuzz harness tests are legitimately larger than the production harness they drive. The 263-line test file uses for loops over fixture arrays to auto-generate hundreds of cases across 10 describe sections. This is idiomatic for property-based testing and actually demonstrates strong coverage discipline, not waste.

Suggestion: No action required. The ratio of 3.75:1 is expected for a combinatorial test harness. If the team wants to suppress this flag in future runs, a note in the file header (e.g. // property-based harness — inflation ratio expected) would provide useful context.


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 2 files — both //go:build !integration
  • 🟨 JavaScript (*.test.cjs): 2 files (vitest)

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All new tests verify behavioral contracts of isSafeExpression — safe vs. unsafe expression classification across standalone literals, comparisons, AND/OR compounds, ternary chains, and exhaustive security invariants. 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: §25283421282

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

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%). All new tests verify behavioral contracts of isSafeExpression across standalone literals, comparisons, AND/OR compounds, ternary chains, and exhaustive security invariants.

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

This PR updates the JavaScript runtime-import expression validator to align with the Go-side validator so that spec-enforcer.md can safely use ternary-style &&/|| patterns without failing activation-time validation.

Changes:

  • Extend isSafeExpression (JS) to accept standalone literals, && expressions, and simple comparisons.
  • Expand Go and JS tests (unit + fuzz seeds) to cover the new compound-expression patterns.
  • Add a JS fuzz harness + property-based tests to exercise security invariants for isSafeExpression.
Show a summary per file
File Description
actions/setup/js/runtime_import.cjs Extends JS isSafeExpression to allow literals / && / comparisons, and fixes OR/AND fall-through behavior.
actions/setup/js/runtime_import.test.cjs Adds JS unit tests for newly supported expression forms.
actions/setup/js/fuzz_is_safe_expression_harness.cjs Adds a stdin-driven harness to call isSafeExpression for fuzzing/property tests.
actions/setup/js/fuzz_is_safe_expression_harness.test.cjs Adds property-based tests validating the new expression forms and security boundaries.
pkg/parser/runtime_import_test.go Adds Go table tests to exercise JS runtime validator for compound expressions.
pkg/parser/runtime_import_fuzz_test.go Expands fuzz seed corpus and adds a new invariant around unsafe leading namespaces.

Copilot's findings

Tip

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

  • Files reviewed: 6/6 changed files
  • Comments generated: 5

Comment on lines +108 to +112
name: "comparison expression with safe property",
expression: "github.event.inputs.enforce_all == 'true'",
expectSafe: true,
description: "Comparison with safe LHS property is allowed",
},
Comment on lines +79 to +83
const { safe, error } = testIsSafeExpression(`${left} ${op} 'value'`);
expect(error).toBeNull();
expect(safe).toBe(true);
});
}
Comment thread actions/setup/js/runtime_import.cjs Outdated
Comment on lines +204 to +221
const isStringLiteralStandalone = /^(['"`]).*\1$/.test(trimmed);
if (isStringLiteralStandalone) {
const contentMatch = trimmed.match(/^(['"`])(.+)\1$/);
if (contentMatch) {
const content = contentMatch[2];
// Reject nested expressions
if (content.includes("${{") || content.includes("}}")) {
return false;
}
// Reject escape sequences that could hide keywords
if (/\\[xu][\da-fA-F]/.test(content) || /\\[0-7]{1,3}/.test(content)) {
return false;
}
// Reject zero-width characters
if (/[\u200B-\u200D\uFEFF]/.test(content)) {
return false;
}
}
Comment on lines +302 to 316
// Check for simple comparison expressions (e.g., "github.event.inputs.enforce_all == 'true'").
// This check only runs for expressions that have no top-level || or && operators (since those
// cases are fully handled above), preventing a partially-validated compound expression from
// sneaking through via the comparison path.
// Extract each property access on the left side of a comparison operator and verify it is in
// the allowed list. This mirrors the Go comparisonExtractionRegex logic.
const compExtractRegex = /([a-zA-Z_][a-zA-Z0-9_.]*)\s*(?:==|!=|<=?|>=?)\s*/g;
const comparisonProps = [];
let compMatch;
while ((compMatch = compExtractRegex.exec(trimmed)) !== null) {
comparisonProps.push(compMatch[1].trim());
}
if (comparisonProps.length > 0 && comparisonProps.every(prop => isSafeExpression(prop))) {
return true;
}
Comment on lines +326 to +330
it("should allow comparison expressions with safe properties", () => {
expect(isSafeExpression("github.event.inputs.enforce_all == 'true'")).toBe(!0);
expect(isSafeExpression("github.actor == 'octocat'")).toBe(!0);
expect(isSafeExpression("inputs.mode != 'dry-run'")).toBe(!0);
});
@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented May 3, 2026

@copilot this expression links incomplete

${{ github.event.inputs.enforce_all == 'true' && 'full-sweep (enforce_all)' || 'round-robin' }}

Refuse constant literal string subexpressioj in a disjunction/conjunction

Copilot AI and others added 2 commits May 3, 2026 16:13
… regex; fix spec-enforcer expression

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d281a45c-1fea-437c-96af-a7570162b2cf

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
… invariant fuzz tests

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d281a45c-1fea-437c-96af-a7570162b2cf

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 3, 2026

@copilot this expression links incomplete

${{ github.event.inputs.enforce_all == 'true' && 'full-sweep (enforce_all)' || 'round-robin' }}...

Done in commit 5e438d6 (with follow-up cleanup in 8f9d307):

isSafeExpression changes:

  • Refuse literal sub-expressions in &&: cond && 'literal' and 'literal' && cond are now both rejected. The ternary idiom cond && 'yes' || 'no' is therefore also rejected.
  • Refuse literal on the left of ||: 'default' || expr is rejected (always-truthy left side makes the right dead code). The established OR-fallback pattern expr || 'default' still works.
  • Tighten the string-literal regex: Changed from /^(['"\]).\1$/to/^'(?:[^'\\]|\.)'$|.../— prevents compound expressions like'a' || secrets.TOKEN || 'b'from being misclassified as a single string literal (they start and end with'` but contain unescaped inner quotes).

spec-enforcer.md line 389 updated to the valid OR-fallback form:

${{ github.event.inputs.enforce_all || 'round-robin' }}

Tests updated across runtime_import.test.cjs, fuzz_is_safe_expression_harness.test.cjs, and the Go fuzz/unit tests to reflect the new semantics (295 JS tests pass).

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/actions/github-script/git/ref/tags/v9
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v9 --jq [.object.sha, .object.type] | @tsv GOSUMDB GOWORK 64/bin/go GOINSECURE GOMOD GOMODCACHE sh -c npx prettier --cGOINSECURE GOPROXY 64/bin/go GOSUMDB GOWORK 64/bin/go node (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title fix: support AND expressions and comparisons in isSafeExpression to unblock spec-enforcer fix: harden isSafeExpression — refuse literal operands in &&/|| and fix spec-enforcer expression May 3, 2026
@pelikhan pelikhan merged commit 6a191ad into main May 3, 2026
@pelikhan pelikhan deleted the copilot/fix-package-specification-enforcer branch May 3, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[aw-failures] Fix Package Specification Enforcer — ERR_VALIDATION: unauthorized GitHub Actions expressions in spec-enforcer.md

3 participants