Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.util.Set;
import java.util.stream.Collectors;
import javax.net.ssl.SSLException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
Expand Down Expand Up @@ -106,6 +107,22 @@
*/
@Slf4j
public class HttpCollectImpl extends AbstractCollect {

/**
* Pre-compiled regex patterns for dangerous XPath detection.
* Compiled once at class load for performance.
*/
private static final List<Pattern> DANGEROUS_XPATH_PATTERNS;

static {
List<Pattern> patterns = new ArrayList<>();
for (String pattern : CollectorConstants.DANGEROUS_XPATH_PATTERNS) {
// Use CASE_INSENSITIVE to prevent bypass via case variations (e.g., //TEXT() vs //text())
patterns.add(Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
}
DANGEROUS_XPATH_PATTERNS = Collections.unmodifiableList(patterns);
}

private final Set<Integer> defaultSuccessStatusCodes = Set.of(
HttpStatus.SC_OK,
HttpStatus.SC_CREATED,
Expand Down Expand Up @@ -357,21 +374,83 @@ private void parseResponseBySiteMap(String resp, List<String> aliasFields,
}
}

/**
* Validates the XPath expression to prevent DoS attacks.
* Checks for dangerous patterns that could traverse the entire XML document.
* Uses pre-compiled patterns for performance and case-insensitive matching for security.
*
* @param xpathExpression the XPath expression to validate
* @throws IllegalArgumentException if the expression contains dangerous patterns
*/
private void validateXPathExpression(String xpathExpression) throws IllegalArgumentException {
if (!StringUtils.hasText(xpathExpression)) {
return;
}

// Check against dangerous patterns using pre-compiled regex
for (Pattern pattern : DANGEROUS_XPATH_PATTERNS) {
Matcher matcher = pattern.matcher(xpathExpression);
if (matcher.find()) {
throw new IllegalArgumentException(
"XPath expression contains dangerous pattern that may cause DoS: " + pattern.pattern()
);
}
}

// Check for excessive wildcard usage (more than 3 // or * operators)
long descendantAxisCount = xpathExpression.chars().filter(ch -> ch == '/').count();
long wildcardCount = xpathExpression.chars().filter(ch -> ch == '*').count();

Comment on lines +400 to +403

Copilot AI Jan 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "Check for excessive wildcard usage (more than 3 // or * operators)" does not match the implementation: the code counts every '/' (not specifically the '//' descendant axis) and uses thresholds of 10 for '/' and 5 for '' instead of 3. Please either adjust the thresholds/logic to match the documented rule, or update the comment so it accurately describes the condition being enforced (e.g., clarify it's counting total '/' and '' characters with the given limits).

Suggested change
// Check for excessive wildcard usage (more than 3 // or * operators)
long descendantAxisCount = xpathExpression.chars().filter(ch -> ch == '/').count();
long wildcardCount = xpathExpression.chars().filter(ch -> ch == '*').count();
// Check for excessive traversal and wildcard usage by counting total '/' and '*' characters.
long descendantAxisCount = xpathExpression.chars().filter(ch -> ch == '/').count();
long wildcardCount = xpathExpression.chars().filter(ch -> ch == '*').count();
// Heuristic limits: more than 10 '/' characters or more than 5 '*' characters is considered risky.

Copilot uses AI. Check for mistakes.
if (descendantAxisCount > 10 || wildcardCount > 5) {
throw new IllegalArgumentException(
"XPath expression contains too many wildcards or descendant axes, potential DoS risk"
);
}

log.debug("XPath expression validation passed: {}", xpathExpression);
}

private void parseResponseByXmlPath(String resp, Metrics metrics,
CollectRep.MetricsData.Builder builder, Long responseTime) {
HttpProtocol http = metrics.getHttp();
List<String> aliasFields = metrics.getAliasFields();
String xpathExpression = http.getParseScript();

// Layer 1: Validate XPath expression is not empty
if (!StringUtils.hasText(xpathExpression)) {
log.warn("Http collect parse type is xmlPath, but the xpath expression is empty.");
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("XPath expression is empty");
return;
}

// Layer 2: Check XML response size to prevent memory exhaustion
if (resp != null && resp.length() > CollectorConstants.MAX_XML_RESPONSE_SIZE) {
log.warn("XML response size {} bytes exceeds maximum allowed size {} bytes",
resp.length(), CollectorConstants.MAX_XML_RESPONSE_SIZE);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("XML response exceeds maximum allowed size of "
+ (CollectorConstants.MAX_XML_RESPONSE_SIZE / 1024 / 1024) + "MB");
return;
Comment on lines +428 to +434

Copilot AI Jan 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_XML_RESPONSE_SIZE is documented as a byte limit, but here it's compared against resp.length() and logged as "bytes", which is the number of Java characters, not the original UTF-8 payload size. For XML with many multibyte characters this can allow responses substantially larger than the intended 10MB while still reporting them as within the byte limit. Consider either measuring the actual byte length (e.g., via the original HttpEntity or re-encoding with the correct charset) or updating the constant's Javadoc and log/message text to make it clear this is a character-count limit instead of bytes.

Suggested change
if (resp != null && resp.length() > CollectorConstants.MAX_XML_RESPONSE_SIZE) {
log.warn("XML response size {} bytes exceeds maximum allowed size {} bytes",
resp.length(), CollectorConstants.MAX_XML_RESPONSE_SIZE);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("XML response exceeds maximum allowed size of "
+ (CollectorConstants.MAX_XML_RESPONSE_SIZE / 1024 / 1024) + "MB");
return;
if (resp != null) {
int respSizeBytes = resp.getBytes(StandardCharsets.UTF_8).length;
if (respSizeBytes > CollectorConstants.MAX_XML_RESPONSE_SIZE) {
log.warn("XML response size {} bytes exceeds maximum allowed size {} bytes",
respSizeBytes, CollectorConstants.MAX_XML_RESPONSE_SIZE);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("XML response exceeds maximum allowed size of "
+ (CollectorConstants.MAX_XML_RESPONSE_SIZE / 1024 / 1024) + "MB");
return;
}

Copilot uses AI. Check for mistakes.
}

// Layer 3: Validate XPath expression for dangerous patterns
try {
validateXPathExpression(xpathExpression);
} catch (IllegalArgumentException e) {
log.warn("XPath expression validation failed: {}", e.getMessage());
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(e.getMessage());
return;
}

int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword());

try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

// Layer 4: Enable XML secure processing and XXE protection
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Expand Down Expand Up @@ -408,7 +487,16 @@ private void parseResponseByXmlPath(String resp, Metrics metrics,
return;
}

for (int i = 0; i < nodeList.getLength(); i++) {
// Layer 5: Limit the number of results to prevent excessive resource consumption
int resultSize = nodeList.getLength();
int maxResults = CollectorConstants.MAX_XPATH_RESULT_NODES;
if (resultSize > maxResults) {
log.warn("XPath expression returned {} nodes, exceeding limit of {}. Processing first {} nodes only.",
resultSize, maxResults, maxResults);
resultSize = maxResults;
}

for (int i = 0; i < resultSize; i++) {
Node node = nodeList.item(i);
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();

Expand Down Expand Up @@ -882,4 +970,4 @@ private boolean checkSuccessInvoke(Metrics metrics, int statusCode) {
}
return successCodeSet.contains(statusCode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,28 @@ public interface CollectorConstants extends NetworkConstants {

String STATUS_CODE = "statusCode";

/**
* Maximum XML response size in bytes (10MB) to prevent DoS attacks
*/
int MAX_XML_RESPONSE_SIZE = 10 * 1024 * 1024;

/**
* Maximum number of nodes returned by XPath query to prevent excessive resource consumption
*/
int MAX_XPATH_RESULT_NODES = 1000;

/**
* Dangerous XPath expression patterns that could cause DoS attacks
* These patterns match expressions that traverse the entire XML document
*/
String[] DANGEROUS_XPATH_PATTERNS = {
"//\\*\\s*\\|\\s*//@\\*\\s*\\|\\s*//text\\(\\)", // //* | //@* | //text()
"//\\*\\s*\\|", // //* | ...
"//@\\*\\s*\\|", // //@* | ...
"//node\\(\\)\\s*\\|", // //node() | ...
"descendant-or-self::node\\(\\)\\s*\\|", // descendant-or-self::node() | ...
"/descendant-or-self::node\\(\\)", // /descendant-or-self::node()
"//\\*[\\s\\S]*//\\*" // //** with multiple wildcards

Copilot AI Jan 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline comment // //** with multiple wildcards does not precisely describe the regex pattern "//\\*[\\s\\S]*//\\*" (which matches two //* segments with arbitrary content between them, not literally //**). To avoid confusion when maintaining these security-sensitive patterns, consider rephrasing the comment to more accurately describe what the regex is matching.

Suggested change
"//\\*[\\s\\S]*//\\*" // //** with multiple wildcards
"//\\*[\\s\\S]*//\\*" // //* ... //* (two wildcard-descendant segments with arbitrary content between)

Copilot uses AI. Check for mistakes.
};

}
Loading