CVE-2023-36812 (published 2023-06-30T23:15Z)

OpenTSDB is a open source, distributed, scalable Time Series Database (TSDB). OpenTSDB is vulnerable to Remote Code Execution vulnerability by writing user-controlled input to Gnuplot configuration file and running Gnuplot with the generated configuration. This issue has been patched in  commit `07c4641471c` and further refined in commit `fa88d3e4b`. These patches are available in the `2.4.2` release. Users are advised to upgrade. User unable to upgrade may disable Gunuplot via the config option`tsd.core.enable_ui = true` and remove the shell files `mygnuplot.bat` and `mygnuplot.sh`.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-74
cwe_name: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
extended_description: Software or other automated logic has certain assumptions about what constitutes data and control respectively. It is the lack of verification of these assumptions for user-controlled input that leads to injection problems. Injection problems encompass a wide variety of issues -- all mitigated in very different ways and usually attempted in order to alter the control flow of the process. For this reason, the most effective way to discuss these weaknesses is to note the distinct features that classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed.
         

Fix information:
repo_url: https://github.com/OpenTSDB/opentsdb
commit_details: repo_url: https://github.com/OpenTSDB/opentsdb
author: Chris Larsen
author_date: 2023-04-10 21:34:22-07:00
msg: Fix for #2269 and #2267 XSS vulnerability.
Escaping the user supplied input when outputing the HTML for the old BadRequest
HTML handlers should help. Thanks to the reporters.
Fixes CVE-2018-13003.
repository: repo_url: https://github.com/OpenTSDB/opentsdb
repo_name: OpenTSDB/opentsdb
description: A scalable, distributed Time Series Database.
repo_language: Java
owner: OpenTSDB
file_changes: filename: HttpQuery.java
change_type: ModificationType.MODIFY
diff: @@ -25,6 +25,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 
+import com.google.common.html.HtmlEscapers;
 import net.opentsdb.core.Const;
 import net.opentsdb.core.TSDB;
 import net.opentsdb.graph.Plot;
@@ -373,6 +374,10 @@ final class HttpQuery extends AbstractHttpQuery {
       buf.append("\"}");
       sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, buf);
     } else {
+      String response = "";
+      if (pretty_exc != null) {
+        response = HtmlEscapers.htmlEscaper().escape(pretty_exc);
+      }
       sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR,
                 makePage("Internal Server Error", "Houston, we have a problem",
                          "<blockquote>"
@@ -380,7 +385,7 @@ final class HttpQuery extends AbstractHttpQuery {
                          + "Oops, sorry but your request failed due to a"
                          + " server error.<br/><br/>"
                          + "Please try again in 30 seconds.<pre>"
-                         + pretty_exc
+                         + response
                          + "</pre></blockquote>"));
     }
   }
@@ -420,6 +425,10 @@ final class HttpQuery extends AbstractHttpQuery {
       buf.append("\"}");
       sendReply(HttpResponseStatus.BAD_REQUEST, buf);
     } else {
+      String response = "";
+      if (exception.getMessage() != null) {
+        response = HtmlEscapers.htmlEscaper().escape(exception.getMessage());
+      }
       sendReply(HttpResponseStatus.BAD_REQUEST,
                 makePage("Bad Request", "Looks like it's your fault this time",
                          "<blockquote>"
@@ -427,7 +436,7 @@ final class HttpQuery extends AbstractHttpQuery {
                          + "Sorry but your request was rejected as being"
                          + " invalid.<br/><br/>"
                          + "The reason provided was:<blockquote>"
-                         + exception.getMessage()
+                         + response
                          + "</blockquote></blockquote>"));
     }
   }

programming_language: Java
filename: TestHttpQuery.java
change_type: ModificationType.MODIFY
diff: @@ -795,6 +795,18 @@ public final class TestHttpQuery {
         query.response().getContent().toString(Charset.forName("UTF-8"))
         .substring(0, 15));
   }
+
+  @Test
+  public void internalErrorDeprecatedHTMLEscaped() {
+    HttpQuery query = NettyMocks.getQuery(tsdb, "");
+    query.internalError(new Exception("<script>alert(document.cookie)</script>"));
+
+    assertEquals(HttpResponseStatus.INTERNAL_SERVER_ERROR,
+        query.response().getStatus());
+    assertTrue(query.response().getContent().toString(Charset.forName("UTF-8")).contains(
+        "&lt;script&gt;alert(document.cookie)&lt;/script&gt;"
+    ));
+  }
   
   @Test
   public void internalErrorDeprecatedJSON() {
@@ -849,6 +861,17 @@ public final class TestHttpQuery {
         query.response().getContent().toString(Charset.forName("UTF-8"))
         .substring(0, 15));
   }
+
+  @Test
+  public void badRequestDeprecatedHTMLEscaped() {
+    HttpQuery query = NettyMocks.getQuery(tsdb, "/");
+    query.badRequest(new BadRequestException("<script>alert(document.cookie)</script>"));
+
+    assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus());
+    assertTrue(query.response().getContent().toString(Charset.forName("UTF-8")).contains(
+        "The reason provided was:<blockquote>&lt;script&gt;alert(document.cookie)&lt;/script&gt;"
+    ));
+  }
   
   @Test
   public void badRequestDeprecatedJSON() {

programming_language: Java
filename: TestQueryRpc.java
change_type: ModificationType.MODIFY
diff: @@ -518,7 +518,7 @@ public final class TestQueryRpc {
     assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus());
     final String json =
         query.response().getContent().toString(Charset.forName("UTF-8"));
-    assertTrue(json.contains("No such name for 'foo': 'metrics'"));
+    assertTrue(json.contains("No such name for &#39;foo&#39;: &#39;metrics&#39;"));
   }
 
   @Test
@@ -579,7 +579,7 @@ public final class TestQueryRpc {
     assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus());
     final String json =
         query.response().getContent().toString(Charset.forName("UTF-8"));
-    assertTrue(json.contains("No such name for 'foo': 'metrics'"));
+    assertTrue(json.contains("No such name for &#39;foo&#39;: &#39;metrics&#39;"));
   }
 
   @Test

programming_language: Java
repo_url: https://github.com/OpenTSDB/opentsdb
commit_details: repo_url: https://github.com/OpenTSDB/opentsdb
author: Chris Larsen
author_date: 2023-04-10 22:20:30-07:00
msg: Improved fix for #2261.
Regular expressions wouldn't catch the newlines or possibly other
control characters. Now we'll use the TAG validation code to make
sure the inputs are only plain ASCII printables first.
Fixes CVE-2018-12972, CVE-2020-35476
repository: repo_url: https://github.com/OpenTSDB/opentsdb
repo_name: OpenTSDB/opentsdb
description: A scalable, distributed Time Series Database.
repo_language: Java
owner: OpenTSDB
file_changes: filename: GraphHandler.java
change_type: ModificationType.MODIFY
diff: @@ -40,15 +40,17 @@ import com.fasterxml.jackson.databind.JsonMappingException;
 import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
+import net.opentsdb.core.*;
 import net.opentsdb.core.Const;
 import net.opentsdb.core.DataPoint;
 import net.opentsdb.core.DataPoints;
 import net.opentsdb.core.Query;
 import net.opentsdb.core.TSDB;
 import net.opentsdb.core.TSQuery;
+import net.opentsdb.core.Tags;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import net.opentsdb.graph.Plot;
 import net.opentsdb.meta.Annotation;
 import net.opentsdb.stats.Histogram;
@@ -667,6 +669,7 @@ final class GraphHandler implements HttpRpc {
     String wxh = query.getQueryStringParam("wxh");
     if (wxh != null && !wxh.isEmpty()) {
       wxh = URLDecoder.decode(wxh.trim());
+      validateString("wxh", wxh);
       if (!WXH_VALIDATOR.matcher(wxh).find()) {
         throw new IllegalArgumentException("'wxh' was invalid. "
             + "Must satisfy the pattern " + WXH_VALIDATOR.toString());
@@ -744,6 +747,7 @@ final class GraphHandler implements HttpRpc {
     final Map<String, List<String>> querystring = query.getQueryString();
     String value;
     if ((value = popParam(querystring, "yrange")) != null) {
+      validateString("yrange", value, "[:]");
       if (!RANGE_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'yrange' was invalid. "
             + "Must be in the format [min:max].");
@@ -751,6 +755,7 @@ final class GraphHandler implements HttpRpc {
       params.put("yrange", value);
     }
     if ((value = popParam(querystring, "y2range")) != null) {
+      validateString("y2range", value, "[:]");
       if (!RANGE_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'y2range' was invalid. "
             + "Must be in the format [min:max].");
@@ -758,6 +763,7 @@ final class GraphHandler implements HttpRpc {
       params.put("y2range", value);
     }
     if ((value = popParam(querystring, "ylabel")) != null) {
+      validateString("ylabel", value, " ");
       if (!LABEL_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'ylabel' was invalid. Must "
             + "satisfy the pattern " + LABEL_VALIDATOR.toString());
@@ -765,6 +771,7 @@ final class GraphHandler implements HttpRpc {
       params.put("ylabel", stringify(value));
     }
     if ((value = popParam(querystring, "y2label")) != null) {
+      validateString("y2label", value, " ");
       if (!LABEL_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'y2label' was invalid. Must "
             + "satisfy the pattern " + LABEL_VALIDATOR.toString());
@@ -772,6 +779,7 @@ final class GraphHandler implements HttpRpc {
       params.put("y2label", stringify(value));
     }
     if ((value = popParam(querystring, "yformat")) != null) {
+      validateString("yformat", value, "% ");
       if (!FORMAT_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'yformat' was invalid. Must "
             + "satisfy the pattern " + FORMAT_VALIDATOR.toString());
@@ -779,6 +787,7 @@ final class GraphHandler implements HttpRpc {
       params.put("format y", stringify(value));
     }
     if ((value = popParam(querystring, "y2format")) != null) {
+      validateString("y2format", value, "% ");
       if (!FORMAT_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'y2format' was invalid. Must "
             + "satisfy the pattern " + FORMAT_VALIDATOR.toString());
@@ -786,6 +795,7 @@ final class GraphHandler implements HttpRpc {
       params.put("format y2", stringify(value));
     }
     if ((value = popParam(querystring, "xformat")) != null) {
+      validateString("xformat", value, "% ");
       if (!FORMAT_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'xformat' was invalid. Must "
             + "satisfy the pattern " + FORMAT_VALIDATOR.toString());
@@ -799,6 +809,7 @@ final class GraphHandler implements HttpRpc {
       params.put("logscale y2", "");
     }
     if ((value = popParam(querystring, "key")) != null) {
+      validateString("key", value);
       if (!KEY_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'key' was invalid. Must "
             + "satisfy the pattern " + KEY_VALIDATOR.toString());
@@ -806,6 +817,7 @@ final class GraphHandler implements HttpRpc {
       params.put("key", value);
     }
     if ((value = popParam(querystring, "title")) != null) {
+      validateString("title", value, " ");
       if (!LABEL_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'title' was invalid. Must "
             + "satisfy the pattern " + LABEL_VALIDATOR.toString());
@@ -813,6 +825,7 @@ final class GraphHandler implements HttpRpc {
       params.put("title", stringify(value));
     }
     if ((value = popParam(querystring, "bgcolor")) != null) {
+      validateString("bgcolor", value);
       if (!COLOR_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'bgcolor' was invalid. Must "
             + "be a hex value e.g. 'xFFFFFF'");
@@ -820,6 +833,7 @@ final class GraphHandler implements HttpRpc {
       params.put("bgcolor", value);
     }
     if ((value = popParam(querystring, "fgcolor")) != null) {
+      validateString("fgcolor", value);
       if (!COLOR_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'fgcolor' was invalid. Must "
             + "be a hex value e.g. 'xFFFFFF'");
@@ -827,6 +841,7 @@ final class GraphHandler implements HttpRpc {
       params.put("fgcolor", value);
     }
     if ((value = popParam(querystring, "smooth")) != null) {
+      validateString("smooth", value);
       if (!SMOOTH_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'smooth' was invalid. Must "
             + "satisfy the pattern " + SMOOTH_VALIDATOR.toString());
@@ -834,6 +849,7 @@ final class GraphHandler implements HttpRpc {
       params.put("smooth", value);
     }
     if ((value = popParam(querystring, "style")) != null) {
+      validateString("style", value);
       if (!STYLE_VALIDATOR.matcher(value).find()) {
         throw new BadRequestException("'style' was invalid. Must "
             + "satisfy the pattern " + STYLE_VALIDATOR.toString());
@@ -1071,4 +1087,26 @@ final class GraphHandler implements HttpRpc {
     LOG.error(query.channel().toString() + ' ' + msg, e);
   }
 
+  static void validateString(final String what, final String s) {
+    validateString(what, s, "");
+  }
+
+  public static void validateString(final String what, final String s, String specials) {
+    if (s == null) {
+      throw new BadRequestException("Invalid " + what + ": null");
+    } else if ("".equals(s)) {
+      throw new BadRequestException("Invalid " + what + ": empty string");
+    }
+    final int n = s.length();
+    for (int i = 0; i < n; i++) {
+      final char c = s.charAt(i);
+      if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
+          || ('0' <= c && c <= '9') || c == '-' || c == '_' || c == '.'
+          || c == '/' || Character.isLetter(c) || specials.indexOf(c) != -1)) {
+        throw new BadRequestException("Invalid " + what
+            + " (\"" + s + "\"): illegal character: " + c);
+      }
+    }
+  }
+
 }

programming_language: Java
filename: TestGraphHandler.java
change_type: ModificationType.MODIFY
diff: @@ -97,6 +97,7 @@ public final class TestGraphHandler {
     assertPlotParam("yrange", "[-10.1e-5:]");
     assertPlotParam("yrange", "[-10.1e-5:-10.1e-6]");
     assertInvalidPlotParam("yrange", "[33:system('touch /tmp/poc.txt')]");
+    assertInvalidPlotParam("y2range", "[42:%0a[33:system('touch /tmp/poc.txt')]");
   }
 
   @Test
@@ -109,7 +110,8 @@ public final class TestGraphHandler {
     assertPlotParam("key", "horiz");
     assertPlotParam("key", "box");
     assertPlotParam("key", "bottom");
-    assertInvalidPlotParam("yrange", "out%20right%20top%0aset%20yrange%20[33:system(%20");
+    assertInvalidPlotParam("key", "out%20right%20top%0aset%20yrange%20[33:system(%20");
+    assertInvalidPlotParam("key", "%3Bsystem%20%22cat%20/home/ubuntuvm/secret.txt%20%3E/tmp/secret.txt%22%20%22");
   }
 
   @Test
@@ -118,16 +120,23 @@ public final class TestGraphHandler {
     assertPlotParam("style", "points");
     assertPlotParam("style", "circles");
     assertPlotParam("style", "dots");
-    assertInvalidPlotParam("style", "dots%20[33:system(%20");
+    assertInvalidPlotParam("style", "dots%20%0a[33:system(%20");
+    assertInvalidPlotParam("style", "%3Bsystem%20%22cat%20/home/ubuntuvm/secret.txt%20%3E/tmp/secret.txt%22%20%22\"");
   }
 
   @Test
   public void setLabelParams() throws Exception {
     assertPlotParam("ylabel", "This is good");
     assertPlotParam("ylabel", " and so Is this - _ yay");
-    assertInvalidPlotParam("ylabel", "[33:system(%20");
-    assertInvalidPlotParam("title", "[33:system(%20");
-    assertInvalidPlotParam("y2label", "[33:system(%20");
+    assertInvalidPlotParam("ylabel", "system(%20no%0anewlines");
+    assertInvalidPlotParam("title", "system(%20no%0anewlines");
+    assertInvalidPlotParam("y2label", "system(%20no%0anewlines");
+  }
+
+  @Test
+  public void setWXH() throws Exception {
+    assertPlotDimension("wxh",  "720x640");
+    assertInvalidPlotDimension("wxh", "720%0ax640");
   }
 
   @Test
@@ -137,12 +146,14 @@ public final class TestGraphHandler {
     assertPlotParam("bgcolor", "%58DEADBE");
     assertInvalidPlotParam("bgcolor", "XDEADBEF");
     assertInvalidPlotParam("bgcolor", "%5BDEADBE");
+    assertInvalidPlotParam("bgcolor", "xBDE%0AAD");
 
     assertPlotParam("fgcolor", "x000000");
     assertPlotParam("fgcolor", "XDEADBE");
     assertPlotParam("fgcolor", "%58DEADBE");
     assertInvalidPlotParam("fgcolor", "XDEADBEF");
     assertInvalidPlotParam("fgcolor", "%5BDEADBE");
+    assertInvalidPlotParam("fgcolor", "xBDE%0AAD");
   }
 
   @Test
@@ -160,7 +171,8 @@ public final class TestGraphHandler {
     assertPlotParam("smooth", "sbezier");
     assertPlotParam("smooth", "unwrap");
     assertPlotParam("smooth", "zsort");
-    assertInvalidPlotParam("smooth", "[33:system(%20");
+    assertInvalidPlotParam("smooth", "bezier%20system(%20");
+    assertInvalidPlotParam("smooth", "fnormal%0asystem(%20");
   }
 
   @Test
@@ -172,7 +184,8 @@ public final class TestGraphHandler {
     assertPlotParam("yformat", "%253.0em%25%25");
     assertPlotParam("yformat", "%25.2f seconds");
     assertPlotParam("yformat", "%25.0f ms");
-    assertInvalidPlotParam("yformat", "%252.[33:system");
+    assertInvalidPlotParam("yformat", "%252.system(%20");
+    assertInvalidPlotParam("yformat", "%252.%0asystem(%20");
   }
 
   @Test  // If the file doesn't exist, we don't use it, obviously.
@@ -344,6 +357,13 @@ public final class TestGraphHandler {
     GraphHandler.setPlotParams(query, plot);
   }
 
+  private static void assertPlotDimension(String param, String value) {
+    Plot plot = mock(Plot.class);
+    HttpQuery query = mock(HttpQuery.class);
+    when(query.getQueryStringParam(param)).thenReturn(value);
+    GraphHandler.setPlotParams(query, plot);
+  }
+
   private static void assertInvalidPlotParam(String param, String value) {
     Plot plot = mock(Plot.class);
     HttpQuery query = mock(HttpQuery.class);
@@ -357,4 +377,14 @@ public final class TestGraphHandler {
     } catch (BadRequestException e) { }
   }
 
+  private static void assertInvalidPlotDimension(String param, String value) {
+    Plot plot = mock(Plot.class);
+    HttpQuery query = mock(HttpQuery.class);
+    when(query.getQueryStringParam(param)).thenReturn(value);
+    try {
+      GraphHandler.setPlotDimensions(query, plot);
+      fail("Expected BadRequestException");
+    } catch (BadRequestException e) { }
+  }
+
 }

programming_language: Java