VYPR
Critical severityNVD Advisory· Published May 3, 2023· Updated Feb 13, 2025

Remote Code Execution in OpenTSDB

CVE-2023-25826

Description

CVE-2023-25824 is an OS command injection in OpenTSDB's legacy HTTP query API due to incomplete fix for CVE-2020-35476, allowing unauthenticated RCE.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2023-25824 is an OS command injection in OpenTSDB's legacy HTTP query API due to incomplete fix for CVE-2020-35476, allowing unauthenticated RCE.

Vulnerability

Description CVE-2023-25826 is an improper input validation vulnerability in OpenTSDB, an open-source time series database. The flaw exists in the legacy HTTP query API (/q endpoint) because the application accepts user-supplied input for parameters such as key, style, and smooth without adequate sanitization. When these parameters are passed to a graph-generation shell script, crafted OS commands included in the input are executed on the host system [1][4]. This vulnerability is a direct result of an incomplete fix for the previously disclosed CVE-2020-35476; the regular-expression-based validation added at that time does not effectively filter out control characters like newlines, allowing crafted payloads to bypass the intended restrictions [2].

Attack

Vector An attacker can send HTTP requests to the affected /q endpoint with specially crafted values in the key, style, or smooth parameters. No authentication is required to access the legacy HTTP query API. The injected commands are executed in the context of the OpenTSDB server process, meaning an unauthenticated remote attacker can achieve arbitrary command execution on the underlying host [1][4]. The exploit is simple in that it leverages the shell script's processing of unsanitized parameters.

Impact

Successful exploitation grants an attacker remote code execution (RCE) on the OpenTSDB host system. This could lead to full compromise of the database server, including data exfiltration, system configuration alteration, or lateral movement within the network. Given that OpenTSDB often runs in operational technology (OT) and monitoring environments, the impact on data availability and integrity could be significant.

Mitigation

The OpenTSDB project has addressed this vulnerability in a pull request that replaces the regex-based validation with a stricter input check allowing only plain ASCII printable characters [2]. Users are strongly advised to update to a version that includes this fix. There is no indication of a workaround other than restricting network access to the /q endpoint or disabling the legacy API if not required.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
net.opentsdb:opentsdbMaven
<= 2.4.1

Affected products

2

Patches

1
07c4641471c6

Improved fix for #2261.

https://github.com/OpenTSDB/opentsdbChris LarsenApr 11, 2023via ghsa-ref
2 files changed · +78 10
  • src/tsd/GraphHandler.java+41 3 modified
    @@ -40,15 +40,17 @@
     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 @@ static void setPlotDimensions(final HttpQuery query, final Plot plot) {
         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,48 +747,55 @@ static void setPlotParams(final HttpQuery query, final Plot plot) {
         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].");
           }
           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].");
           }
           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());
           }
           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());
           }
           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());
           }
           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());
           }
           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,41 +809,47 @@ static void setPlotParams(final HttpQuery query, final Plot plot) {
           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());
           }
           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());
           }
           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'");
           }
           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'");
           }
           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());
           }
           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 @@ static void logError(final HttpQuery query, final String msg,
         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);
    +      }
    +    }
    +  }
    +
     }
    
  • test/tsd/TestGraphHandler.java+37 7 modified
    @@ -97,6 +97,7 @@ public void setYRangeParams() throws Exception {
         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 void setKeyParams() throws Exception {
         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 void setStyleParams() throws Exception {
         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 void setColorParams() throws Exception {
         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 void setSmoothParams() throws Exception {
         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 void setFormatParams() throws Exception {
         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 @@ private static void assertPlotParam(String param, String value) {
         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 @@ private static void assertInvalidPlotParam(String param, String value) {
         } 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) { }
    +  }
    +
     }
    

Vulnerability mechanics

Root cause

"Insufficient input validation: regex validators using `.find()` do not reject newline and control characters, allowing OS command injection via crafted query parameters."

Attack vector

An attacker sends crafted HTTP requests to the OpenTSDB legacy HTTP query API, supplying malicious values for parameters such as `yrange`, `ylabel`, `title`, `key`, `wxh`, `bgcolor`, `smooth`, `style`, or `yformat`. By URL-encoding newline characters (`%0a`) or other control characters into the parameter value, the attacker bypasses the existing regex validators (which use `.find()` and do not anchor to the full string). The injected control characters allow the attacker to break out of the intended parameter context and execute arbitrary OS commands on the host system. No authentication is required if the API endpoint is exposed.

Affected code

The vulnerability resides in `src/tsd/GraphHandler.java` within the `setPlotParams` and `setPlotDimensions` methods. Multiple HTTP query parameters (e.g., `yrange`, `ylabel`, `title`, `key`, `wxh`, `bgcolor`, `smooth`, `style`, `yformat`, etc.) are passed through regex validators that fail to reject newline characters (`%0a`) and other control characters. The patch adds a call to a new `validateString()` method before each existing regex check to enforce that input consists only of ASCII printable characters plus a small set of explicitly allowed specials.

What the fix does

The patch introduces a new `validateString()` method in `GraphHandler.java` that iterates over every character of the input and rejects any character that is not an ASCII letter, digit, hyphen, underscore, period, slash, a Unicode letter, or an explicitly allowed special character (e.g., `:`, ` `, `%`). This method is called on every user-supplied parameter *before* the existing regex validator runs. By rejecting newlines (`\n` / `%0a`) and other control characters early, the patch closes the bypass that allowed crafted OS commands to slip past the regex-based validation. The test file `TestGraphHandler.java` is updated with new test cases that confirm inputs containing `%0a` (newline) or semicolons are now correctly rejected.

Preconditions

  • networkThe OpenTSDB HTTP API must be network-accessible to the attacker.
  • authNo authentication is required if the legacy query API endpoint is exposed.
  • inputThe attacker must be able to supply arbitrary query string parameter values.

Generated on May 23, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.