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
8 changes: 4 additions & 4 deletions .github/workflows/spec-enforcer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/spec-enforcer.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ All tests are derived from README.md specifications, not from implementation sou

### Round-Robin State

- **Run mode**: ${{ github.event.inputs.enforce_all == 'true' && 'full-sweep (enforce_all)' || 'round-robin' }}
- **Run mode**: ${{ github.event.inputs.enforce_all || 'round-robin' }}
- **Packages processed this run**: <list>
- **Next packages in rotation**: <list>
- **Total eligible packages**: N (with README.md)
Expand Down
70 changes: 70 additions & 0 deletions actions/setup/js/fuzz_is_safe_expression_harness.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// @ts-check
/**
* Fuzz test harness for isSafeExpression in runtime_import.cjs.
*
* Tests the security invariants of expression validation, covering:
* - Simple allowed expressions (github.*, needs.*, steps.*, etc.)
* - Compound expressions: AND (&&), OR (||), comparisons (==, !=, <=, >=)
* - Ternary-style expressions: condition && 'value' || 'default'
* - Standalone literals: strings, numbers, booleans
* - Security boundaries: secrets.*, vars.*, runner.*, github.token
* - Dangerous prototype-pollution property names
*
* Used by tests and by Go's fuzzing framework when run as main module via stdin.
*/

const { isSafeExpression } = require("./runtime_import.cjs");

/**
* Evaluates isSafeExpression and returns a structured result.
* Never throws — all errors are captured in the error field.
* @param {string} expression - The expression to test (without ${{ }})
* @returns {{ safe: boolean, error: string | null }}
*/
function testIsSafeExpression(expression) {
try {
const safe = isSafeExpression(expression);
return { safe, error: null };
} catch (err) {
return { safe: false, error: err instanceof Error ? err.message : String(err) };
}
}

/**
* Returns true when the expression is known to be unsafe regardless of
* what other sub-expressions it may contain.
* Used by the fuzzer to assert security invariants.
* @param {string} expression
* @returns {boolean}
*/
function containsUnsafeRoot(expression) {
const trimmed = expression.trim();
// Expressions that start with a disallowed namespace are always unsafe.
// We only check "starts with" to avoid false positives on safe sub-expressions.
return /^secrets\./.test(trimmed) || /^runner\./.test(trimmed) || /^vars\./.test(trimmed) || trimmed === "github.token";
}

// Read input from stdin for Go-driven fuzzing
if (require.main === module) {
let input = "";

process.stdin.on("data", chunk => {
input += chunk;
});

process.stdin.on("end", () => {
try {
// Expected JSON: { expression: string }
const { expression } = JSON.parse(input);
const result = testIsSafeExpression(expression ?? "");
process.stdout.write(JSON.stringify(result));
process.exit(0);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
process.stdout.write(JSON.stringify({ safe: false, error: errorMsg }));
process.exit(1);
}
});
}

module.exports = { testIsSafeExpression, containsUnsafeRoot };
Loading