fix(validation): avoid regex stack overflow

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-08 11:34:51 +08:00
parent 824a992afc
commit 7c62aa218a
2 changed files with 81 additions and 10 deletions

View file

@ -20,9 +20,6 @@ public class BasicPrePublishValidator implements PrePublishValidator {
private static final Pattern ASSIGNMENT_WITH_SENSITIVE_KEY = Pattern.compile(
"(?i)(api[_-]?key|access[_-]?key|secret|password|token)\\s*[:=]\\s*(.+)$"
);
private static final Pattern QUOTED_LITERAL = Pattern.compile(
"^(['\"])(.*)\\1(?:\\s*[,;)}\\]])*\\s*(?:(?://|#).*)?$"
);
private static final Pattern IDENTIFIER = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
private static final Pattern BARE_LITERAL = Pattern.compile("[A-Za-z0-9_\\-]{12,}");
private static final Set<String> PLACEHOLDER_MARKERS = Set.of(
@ -112,9 +109,9 @@ public class BasicPrePublishValidator implements PrePublishValidator {
return null;
}
Matcher quotedLiteralMatcher = QUOTED_LITERAL.matcher(rawValue);
if (quotedLiteralMatcher.matches()) {
return quotedLiteralMatcher.group(2);
String quotedLiteral = extractQuotedLiteral(rawValue);
if (quotedLiteral != null) {
return quotedLiteral;
}
rawValue = stripInlineComment(rawValue);
@ -122,9 +119,9 @@ public class BasicPrePublishValidator implements PrePublishValidator {
return null;
}
quotedLiteralMatcher = QUOTED_LITERAL.matcher(rawValue);
if (quotedLiteralMatcher.matches()) {
return quotedLiteralMatcher.group(2);
quotedLiteral = extractQuotedLiteral(rawValue);
if (quotedLiteral != null) {
return quotedLiteral;
}
if (looksLikeExpression(rawValue) || IDENTIFIER.matcher(rawValue).matches()) {
@ -134,6 +131,52 @@ public class BasicPrePublishValidator implements PrePublishValidator {
return BARE_LITERAL.matcher(rawValue).matches() ? rawValue : null;
}
private String extractQuotedLiteral(String rawValue) {
if (rawValue.length() < 2) {
return null;
}
char quote = rawValue.charAt(0);
if (quote != '\'' && quote != '"') {
return null;
}
boolean escaped = false;
for (int i = 1; i < rawValue.length(); i++) {
char current = rawValue.charAt(i);
if (escaped) {
escaped = false;
continue;
}
if (current == '\\') {
escaped = true;
continue;
}
if (current == quote) {
return hasOnlyTrailingSyntax(rawValue, i + 1) ? rawValue.substring(1, i) : null;
}
}
return null;
}
private boolean hasOnlyTrailingSyntax(String rawValue, int startIndex) {
for (int i = startIndex; i < rawValue.length(); i++) {
char current = rawValue.charAt(i);
if (Character.isWhitespace(current) || isTrailingDelimiter(current)) {
continue;
}
if (current == '#') {
return true;
}
return current == '/' && i + 1 < rawValue.length() && rawValue.charAt(i + 1) == '/';
}
return true;
}
private boolean isTrailingDelimiter(char value) {
return value == ',' || value == ';' || value == ')' || value == '}' || value == ']';
}
private String stripInlineComment(String rawValue) {
int hashIndex = rawValue.indexOf('#');
if (hashIndex >= 0) {

View file

@ -7,6 +7,7 @@ import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -109,8 +110,9 @@ class BasicPrePublishValidatorTest {
self._client_secret = credentials.client_secret
access_token = ensure_valid_access_token(session)
headers = build_headers(access_token=access_token)
client_secret = "prefix-" + configured_secret
""".getBytes(StandardCharsets.UTF_8),
229,
275,
"text/x-python"
);
@ -152,4 +154,30 @@ class BasicPrePublishValidatorTest {
assertTrue(result.warnings().stream().anyMatch(warning -> warning.contains("line 3")));
assertTrue(result.warnings().stream().anyMatch(warning -> warning.contains("line 4")));
}
@Test
void shouldScanDeeplyNestedSingleLineObjectWithoutOverflowingRegexStack() {
String content = "const config = "
+ "{ nested: ".repeat(5_000)
+ "{ token: \"literalcredential123\""
+ " }".repeat(5_001)
+ ";";
PackageEntry script = new PackageEntry(
"scripts/deeply-nested.js",
content.getBytes(StandardCharsets.UTF_8),
content.length(),
"text/javascript"
);
PrePublishValidator.SkillPackageContext context = new PrePublishValidator.SkillPackageContext(
List.of(script),
new SkillMetadata("Deeply Nested Skill", "desc", "1.0.0", "body", Map.of()),
"user-1",
1L
);
ValidationResult result = assertDoesNotThrow(() -> validator.validate(context));
assertTrue(result.passed());
assertTrue(result.warnings().stream().anyMatch(warning -> warning.contains("line 1")));
}
}