This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new 88cfd69dfa [fix] prevent SQL injection in QuestDB history queries
(#4259)
88cfd69dfa is described below
commit 88cfd69dfa3ef6741c020ee8d45d9ad21c03f721
Author: aias00 <[email protected]>
AuthorDate: Mon Aug 17 14:03:02 2026 +0800
[fix] prevent SQL injection in QuestDB history queries (#4259)
Co-authored-by: Claude <[email protected]>
Co-authored-by: shown <[email protected]>
Co-authored-by: Duansg <[email protected]>
---
.../history/tsdb/questdb/QuestdbDataStorage.java | 45 +++++++++++++++++++++-
1 file changed, 44 insertions(+), 1 deletion(-)
diff --git
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/questdb/QuestdbDataStorage.java
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/questdb/QuestdbDataStorage.java
index 93f5b017f2..c94ede7ab1 100644
---
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/questdb/QuestdbDataStorage.java
+++
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/questdb/QuestdbDataStorage.java
@@ -32,6 +32,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
@@ -188,6 +189,8 @@ public class QuestdbDataStorage extends
AbstractHistoryDataStorage {
public Map<String, List<Value>> getHistoryMetricData(String instance,
String app, String metrics, String metric, String history) {
String table = this.generateTable(app, metrics, instance);
String dateAdd = getDateAdd(history);
+ validateIdentifier(metric, "metric");
+ validateIdentifier(table, "table");
String selectSql = String.format(QUERY_HISTORY_SQL, metric, table,
dateAdd);
Map<String, List<Value>> instanceValueMap = new HashMap<>(8);
try {
@@ -228,6 +231,8 @@ public class QuestdbDataStorage extends
AbstractHistoryDataStorage {
public Map<String, List<Value>> getHistoryIntervalMetricData(String
instance, String app, String metrics, String metric, String history) {
String table = this.generateTable(app, metrics, instance);
String dateAdd = getDateAdd(history);
+ validateIdentifier(metric, "metric");
+ validateIdentifier(table, "table");
Map<String, List<Value>> instanceValueMap = new HashMap<>(8);
Set<String> instances = new HashSet<>(8);
// query all metric_labels
@@ -247,7 +252,7 @@ public class QuestdbDataStorage extends
AbstractHistoryDataStorage {
instances.add("");
}
for (String instanceValue : instances) {
- String selectSql =
String.format(QUERY_HISTORY_INTERVAL_WITH_INSTANCE_SQL, metric, metric, metric,
metric, table, instanceValue.replace("'", "\\'"), dateAdd);
+ String selectSql =
String.format(QUERY_HISTORY_INTERVAL_WITH_INSTANCE_SQL, metric, metric, metric,
metric, table, escapeStringLiteral(instanceValue), dateAdd);
Map<String, Object> selectResult = executeQuery(selectSql);
if (selectResult == null ||
!selectResult.containsKey("dataset")) {
continue;
@@ -365,6 +370,44 @@ public class QuestdbDataStorage extends
AbstractHistoryDataStorage {
return app + "_" + metrics + "_" + instance;
}
+ /**
+ * Identifier allowlist for QuestDB double-quoted identifiers
(table/column names).
+ * Keeping these to a safe charset prevents breaking out of the surrounding
+ * {@code "..."} quotes in the templated SQL, since none of the allowed
+ * characters can terminate the identifier or introduce SQL syntax.
+ */
+ private static final Pattern IDENTIFIER =
Pattern.compile("^[A-Za-z0-9_-]+$");
+
+ /**
+ * Fail closed when an interpolated SQL identifier carries anything
outside the
+ * safe charset. The {@code metric} and {@code table} values originate
from rest
+ * path variables, so without this check they could inject into the query.
+ *
+ * @param name identifier to validate
+ * @param label human-readable field name for the error message
+ */
+ private static void validateIdentifier(String name, String label) {
+ if (name == null || !IDENTIFIER.matcher(name).matches()) {
+ throw new IllegalArgumentException("QuestDB query rejected:
invalid " + label + " identifier");
+ }
+ }
+
+ /**
+ * Escape a value interpolated into a single-quoted QuestDB string literal.
+ * QuestDB follows the ANSI rule: a single quote is escaped by doubling it
+ * ({@code '} -> {@code ''}). The previous {@code replace("'", "\\'")} was
not
+ * a valid QuestDB escape and left the literal injectable.
+ *
+ * @param value raw value, may be null
+ * @return value safe to place inside {@code '...'}
+ */
+ private static String escapeStringLiteral(String value) {
+ if (value == null) {
+ return "";
+ }
+ return value.replace("'", "''");
+ }
+
private String parseDoubleValue(String value) {
return (new BigDecimal(value)).setScale(4,
RoundingMode.HALF_UP).stripTrailingZeros().toPlainString();
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]