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]

Reply via email to