Validate and sanitize string literals in runtime expression evaluation#14851
Validate and sanitize string literals in runtime expression evaluation#14851
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR enhances the security of string literal validation in runtime_import.cjs by implementing content validation for string literals in OR expressions and escaping extracted literal values to prevent expression injection attacks.
Changes:
- Added validation to reject string literals containing nested expression markers (
${{,}}), escape sequences (\x,\u, octal), and zero-width Unicode characters - Added escaping of
$and{characters in extracted string literal content to neutralize expression markers - Added comprehensive test coverage for the new validation and escaping rules
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| actions/setup/js/runtime_import.cjs | Implemented string literal content validation in isSafeExpression (lines 188-210) and content escaping in evaluateExpression (lines 257-259) |
| actions/setup/js/runtime_import.test.cjs | Added 38 new test cases covering nested expression rejection, escape sequence rejection, zero-width character rejection, safe literal acceptance, and content escaping |
Comments suppressed due to low confidence (1)
actions/setup/js/runtime_import.cjs:190
- The regex pattern uses
.+which doesn't match empty strings, creating an inconsistency with line 187 which uses.*to detect string literals. While empty strings are correctly accepted (they skip validation and return true on line 209), this inconsistency could be confusing. Consider using(.*)instead of(.+)to make the intent explicit, even though both patterns currently produce the same result for empty strings.
const contentMatch = rightExpr.match(/^(['"`])(.+)\1$/);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // If right side is a literal, extract and return it | ||
| const stringLiteralMatch = rightExpr.match(/^(['"`])(.+)\1$/); |
There was a problem hiding this comment.
The regex pattern uses .+ which doesn't match empty string literals like '', "", or ``. This means expressions like inputs.value || '' will not extract the empty string and will instead fall through to line 268, eventually returning \${{ '' }} instead of an empty string. The pattern should use .* to correctly handle empty string literals.
This issue also appears on line 190 of the same file.
| // If right side is a literal, extract and return it | |
| const stringLiteralMatch = rightExpr.match(/^(['"`])(.+)\1$/); | |
| // If right side is a literal, extract and return it (including empty string literals) | |
| const stringLiteralMatch = rightExpr.match(/^(['"`])(.*)\1$/); |
| it("should allow safe string literals", () => { | ||
| // Normal strings should still work | ||
| expect(isSafeExpression("inputs.value || 'normal string'")).toBe(!0); | ||
| expect(isSafeExpression("inputs.value || 'with-dashes_and_underscores'")).toBe(!0); | ||
| // Safe escape sequences like \n, \t should work | ||
| expect(isSafeExpression("inputs.value || 'line\\nbreak'")).toBe(!0); | ||
| expect(isSafeExpression("inputs.value || 'tab\\there'")).toBe(!0); | ||
| }); |
There was a problem hiding this comment.
Missing test coverage for empty string literals. Tests should verify that isSafeExpression("inputs.value || ''") returns true (currently passes but skips validation), and that evaluateExpression("inputs.missing || ''") correctly returns an empty string rather than a wrapped expression (currently fails due to regex bug).
| it("should reject string literals with escape sequences", () => { | ||
| // Reject hex escape sequences (\x) | ||
| expect(isSafeExpression("inputs.value || '\\x41'")).toBe(!1); | ||
| expect(isSafeExpression("inputs.value || 'test\\x20value'")).toBe(!1); | ||
| // Reject unicode escape sequences (\u) | ||
| expect(isSafeExpression("inputs.value || '\\u0041'")).toBe(!1); | ||
| expect(isSafeExpression("inputs.value || 'test\\u0020value'")).toBe(!1); | ||
| // Reject octal escape sequences | ||
| expect(isSafeExpression("inputs.value || '\\101'")).toBe(!1); | ||
| expect(isSafeExpression("inputs.value || 'test\\040value'")).toBe(!1); |
There was a problem hiding this comment.
The test cases assume the regex will match single hex digits after \x or \u, but JavaScript escape sequences require exactly 2 hex digits for \x and exactly 4 for \u. Tests like '\\x41' and '\\u0041' have valid complete sequences, but the regex implementation /\\[xu][\da-fA-F]/ only checks for one hex digit. Consider adding tests for incomplete sequences like '\\x4' and '\\u004' to verify the intended validation behavior once the regex is corrected.
| } | ||
|
|
||
| // Reject escape sequences that could hide keywords | ||
| if (/\\[xu][\da-fA-F]/.test(content) || /\\[0-7]{1,3}/.test(content)) { |
There was a problem hiding this comment.
The regex pattern /\\[xu][\da-fA-F]/ only matches one hex digit after \x or \u, but JavaScript escape sequences require exactly 2 hex digits for \x (e.g., \x41) and exactly 4 hex digits for \u (e.g., \u0041). The pattern should be /\\x[\da-fA-F]{2}|\\u[\da-fA-F]{4}/ to match complete hex and unicode escape sequences. As currently written, it may miss incomplete sequences or flag partial matches incorrectly.
| if (/\\[xu][\da-fA-F]/.test(content) || /\\[0-7]{1,3}/.test(content)) { | |
| if (/(?:\\x[\da-fA-F]{2}|\\u[\da-fA-F]{4})/.test(content) || /\\[0-7]{1,3}/.test(content)) { |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
String literals in OR expressions (
inputs.foo || 'default') were accepted without content validation, allowing potential injection through nested expressions, escape sequences, and zero-width characters.Changes
String Literal Validation (
isSafeExpression):${{or}}markers\x), unicode (\u), and octal escape sequences that could obfuscate keywordsContent Sanitization (
evaluateExpression):$→\$and{→\{in extracted string literalsExample
Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.