fix(validation): preserve credential literal boundaries

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-08 11:45:48 +08:00
parent 7c62aa218a
commit 9d0431f7d3
2 changed files with 45 additions and 15 deletions

View file

@ -17,6 +17,7 @@ import java.util.regex.Pattern;
@Component
public class BasicPrePublishValidator implements PrePublishValidator {
private static final int MIN_GENERIC_SECRET_LENGTH = 12;
private static final Pattern ASSIGNMENT_WITH_SENSITIVE_KEY = Pattern.compile(
"(?i)(api[_-]?key|access[_-]?key|secret|password|token)\\s*[:=]\\s*(.+)$"
);
@ -111,7 +112,7 @@ public class BasicPrePublishValidator implements PrePublishValidator {
String quotedLiteral = extractQuotedLiteral(rawValue);
if (quotedLiteral != null) {
return quotedLiteral;
return quotedLiteral.length() >= MIN_GENERIC_SECRET_LENGTH ? quotedLiteral : null;
}
rawValue = stripInlineComment(rawValue);
@ -121,7 +122,7 @@ public class BasicPrePublishValidator implements PrePublishValidator {
quotedLiteral = extractQuotedLiteral(rawValue);
if (quotedLiteral != null) {
return quotedLiteral;
return quotedLiteral.length() >= MIN_GENERIC_SECRET_LENGTH ? quotedLiteral : null;
}
if (looksLikeExpression(rawValue) || IDENTIFIER.matcher(rawValue).matches()) {
@ -153,24 +154,25 @@ public class BasicPrePublishValidator implements PrePublishValidator {
continue;
}
if (current == quote) {
return hasOnlyTrailingSyntax(rawValue, i + 1) ? rawValue.substring(1, i) : null;
return hasLiteralTerminator(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) == '/';
private boolean hasLiteralTerminator(String rawValue, int startIndex) {
int index = startIndex;
while (index < rawValue.length() && Character.isWhitespace(rawValue.charAt(index))) {
index++;
}
return true;
if (index == rawValue.length()) {
return true;
}
char current = rawValue.charAt(index);
return isTrailingDelimiter(current)
|| current == '#'
|| (current == '/' && index + 1 < rawValue.length() && rawValue.charAt(index + 1) == '/');
}
private boolean isTrailingDelimiter(char value) {

View file

@ -136,8 +136,10 @@ class BasicPrePublishValidatorTest {
github_token = "ghp_abcdefghijklmnopqrstuvwxyz1234"
const token = "javascriptcredential123";
const config = { token: "objectcredential123", };
const options = { token: "multipropertycredential123", endpoint: "/api" };
const escaped = { token: "credential\\\"value123" };
""".getBytes(StandardCharsets.UTF_8),
184,
319,
"text/javascript"
);
@ -153,6 +155,32 @@ class BasicPrePublishValidatorTest {
assertTrue(result.warnings().stream().anyMatch(warning -> warning.contains("line 2")));
assertTrue(result.warnings().stream().anyMatch(warning -> warning.contains("line 3")));
assertTrue(result.warnings().stream().anyMatch(warning -> warning.contains("line 4")));
assertTrue(result.warnings().stream().anyMatch(warning -> warning.contains("line 5")));
assertTrue(result.warnings().stream().anyMatch(warning -> warning.contains("line 6")));
}
@Test
void shouldNotWarnOnEmptyOrShortSensitiveLiterals() {
PackageEntry script = new PackageEntry(
"scripts/defaults.py",
"""
token = ""
client_secret = "short"
password = 'unset'
""".getBytes(StandardCharsets.UTF_8),
58,
"text/x-python"
);
ValidationResult result = validator.validate(new PrePublishValidator.SkillPackageContext(
List.of(script),
new SkillMetadata("Defaults Skill", "desc", "1.0.0", "body", Map.of()),
"user-1",
1L
));
assertTrue(result.passed());
assertTrue(result.warnings().isEmpty());
}
@Test