From 7c62aa218adf8b309435a17637f2af34a21eb172 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Tue, 8 Sep 2026 11:34:51 +0800 Subject: [PATCH] fix(validation): avoid regex stack overflow Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- .../validation/BasicPrePublishValidator.java | 61 ++++++++++++++++--- .../BasicPrePublishValidatorTest.java | 30 ++++++++- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java index c4012791..8e8a914a 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java @@ -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 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) { diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java index e7f5d456..0cca83f6 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java @@ -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"))); + } }