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
35 changes: 33 additions & 2 deletions src/main/java/com/echo/service/ConditionMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ public class ConditionMatcher {
private static final int MAX_JSON_SIZE = 10 * 1024 * 1024; // 10MB
private static final int MAX_REGEX_INPUT_LENGTH = 10000;

/** 單一欄位值長度上限,超過則跳過比對(防止 base64 等大值拖慢匹配) */
private final int maxFieldValueLength;

/** 預設建構子,使用 2MB 上限(供測試及非 Spring 環境使用) */
public ConditionMatcher() {
this(2 * 1024 * 1024);
}

public ConditionMatcher(
@org.springframework.beans.factory.annotation.Value("${echo.matcher.max-field-value-length:2097152}") int maxFieldValueLength) {
this.maxFieldValueLength = maxFieldValueLength;
}

private final ObjectMapper objectMapper = new ObjectMapper();
private final Cache<String, Pattern> patternCache = Caffeine.newBuilder()
.maximumSize(500)
Expand Down Expand Up @@ -515,7 +528,12 @@ private boolean matchXPathPrepared(String condition, Document doc) {
try {
NodeList nodes = findXmlNodes(parsed.field(), doc);
for (int i = 0; i < nodes.getLength(); i++) {
if (matchValue(parsed, nodes.item(i).getTextContent().trim())) {
String text = nodes.item(i).getTextContent().trim();
if (text.length() > maxFieldValueLength) {
log.debug("Skipping XPath match: node value too large ({} chars), condition: {}", text.length(), condition);
continue;
}
if (matchValue(parsed, text)) {
return true;
}
}
Expand All @@ -530,7 +548,12 @@ private boolean matchXmlPrepared(String condition, Document doc) {
try {
NodeList nodes = findXmlNodes(parsed.field(), doc);
for (int i = 0; i < nodes.getLength(); i++) {
if (matchValue(parsed, nodes.item(i).getTextContent().trim())) {
String text = nodes.item(i).getTextContent().trim();
if (text.length() > maxFieldValueLength) {
log.debug("Skipping XML match: node value too large ({} chars), condition: {}", text.length(), condition);
continue;
}
if (matchValue(parsed, text)) {
return true;
}
}
Expand Down Expand Up @@ -564,6 +587,10 @@ private boolean matchJsonPrepared(String condition, JsonNode root) {
return parsed.op() == Op.NOT_EQUAL;
}
String actual = node.isTextual() ? node.asText() : node.toString();
if (actual.length() > maxFieldValueLength) {
log.debug("Skipping match: field value too large ({} chars), condition: {}", actual.length(), condition);
return false;
}
return matchValue(parsed, actual);
} catch (Exception e) {
return false;
Expand All @@ -575,6 +602,10 @@ private boolean matchJsonPath(String condition, String body) {
var parsed = parseCondition(condition);
Object result = JsonPath.read(body, parsed.field);
String actual = result != null ? result.toString() : null;
if (actual != null && actual.length() > maxFieldValueLength) {
log.debug("Skipping JsonPath match: value too large ({} chars), condition: {}", actual.length(), condition);
return false;
}
return matchValue(parsed, actual);
} catch (PathNotFoundException e) {
return false;
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
server:
port: 8080
undertow:
max-http-post-size: 10485760 # 10MB,允許帶 base64 圖片的大 request 進入
threads:
worker: 32 # Undertow worker threads,可用環境變數 SERVER_UNDERTOW_THREADS_WORKER 覆蓋
servlet:
Expand Down Expand Up @@ -117,6 +118,8 @@ echo:
flush-interval-seconds: 5 # 時間觸發間隔(秒)
analysis:
enabled: true # 啟用匹配鏈分析(near-miss / shadowed 偵測)
matcher:
max-field-value-length: 2097152 # 單一欄位值長度上限 (bytes),超過跳過比對,預設 2MB
builtin-account:
self-registration: false # 設為 true 允許未登入使用者自行申請帳號
ldap:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,69 @@ void regex_inputTooLong_shouldReturnFalse() {
void cleanup_shouldNotThrow() {
matcher.cleanup();
}

// ========== maxFieldValueLength 防呆測試 ==========

@Test
void jsonPrepared_fieldValueTooLarge_shouldReturnFalse() {
// 使用小閾值的 matcher 來測試
ConditionMatcher smallMatcher = new ConditionMatcher(100);
String largeValue = "x".repeat(200);
String json = "{\"userId\":\"U001\",\"image\":\"" + largeValue + "\"}";
PreparedBody prepared = smallMatcher.prepareBody(json);

// image 欄位超過 100 字元,應該跳過
assertThat(smallMatcher.matchesPrepared("image=something", null, null, prepared, null, null)).isFalse();
// userId 欄位正常大小,應該正常匹配
assertThat(smallMatcher.matchesPrepared("userId=U001", null, null, prepared, null, null)).isTrue();
}

@Test
void jsonPath_fieldValueTooLarge_shouldReturnFalse() {
ConditionMatcher smallMatcher = new ConditionMatcher(100);
String largeValue = "x".repeat(200);
String json = "{\"data\":{\"image\":\"" + largeValue + "\",\"id\":\"A1\"}}";
PreparedBody prepared = smallMatcher.prepareBody(json);

// JsonPath 取到的值超過閾值
assertThat(smallMatcher.matchesPrepared("$.data.image=something", null, null, prepared, null, null)).isFalse();
// 正常欄位
assertThat(smallMatcher.matchesPrepared("$.data.id=A1", null, null, prepared, null, null)).isTrue();
}

@Test
void xmlPrepared_nodeValueTooLarge_shouldReturnFalse() {
ConditionMatcher smallMatcher = new ConditionMatcher(100);
String largeValue = "x".repeat(200);
String xml = "<root><image>" + largeValue + "</image><id>A1</id></root>";
PreparedBody prepared = smallMatcher.prepareBody(xml);

// XML 節點值超過閾值
assertThat(smallMatcher.matchesPrepared("image=something", null, null, prepared, null, null)).isFalse();
// 正常節點
assertThat(smallMatcher.matchesPrepared("id=A1", null, null, prepared, null, null)).isTrue();
}

@Test
void xpathPrepared_nodeValueTooLarge_shouldReturnFalse() {
ConditionMatcher smallMatcher = new ConditionMatcher(100);
String largeValue = "x".repeat(200);
String xml = "<root><image>" + largeValue + "</image><id>A1</id></root>";
PreparedBody prepared = smallMatcher.prepareBody(xml);

// XPath 取到的值超過閾值
assertThat(smallMatcher.matchesPrepared("//image=something", null, null, prepared, null, null)).isFalse();
// 正常 XPath
assertThat(smallMatcher.matchesPrepared("//id=A1", null, null, prepared, null, null)).isTrue();
}

@Test
void defaultMatcher_2MB_shouldAllowNormalFields() {
// 預設 matcher (2MB 閾值) 對正常欄位不受影響
String json = "{\"userId\":\"U001\",\"name\":\"test\"}";
PreparedBody prepared = matcher.prepareBody(json);

assertThat(matcher.matchesPrepared("userId=U001", null, null, prepared, null, null)).isTrue();
assertThat(matcher.matchesPrepared("name=test", null, null, prepared, null, null)).isTrue();
}
}
Loading