VYPR
High severityNVD Advisory· Published Nov 11, 2019· Updated Aug 5, 2024

CVE-2019-18841

CVE-2019-18841

Description

Chartkick.js 3.1.0 through 3.1.3, as used in the Chartkick gem before 3.3.0 for Ruby, allows prototype pollution.

AI Insight

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

Chartkick.js 3.1.0 through 3.1.3 allows prototype pollution, enabling attackers to inject arbitrary properties into an Object prototype.

Vulnerability

CVE-2019-18841 is a prototype pollution vulnerability affecting Chartkick.js versions 3.1.0 through 3.1.3, which are used in the Chartkick gem for Ruby before version 3.3.0 [1]. The issue resides in the extend function and the isPlainObject check within the JavaScript library. Specifically, the extend function did not filter out the __proto__ key when copying properties from a source object to a target object, and the isPlainObject check was insufficient to prevent prototype pollution via crafted objects [4].

Exploitation

An attacker can exploit this vulnerability by supplying a specially crafted JSON payload that includes a __proto__ property. When Chartkick processes this data (e.g., chart data from user input, API responses, or other untrusted sources), the __proto__ key is not blocked, allowing the attacker to inject arbitrary properties into the global Object.prototype [4]. The attack does not require authentication if the attacker can control data passed to Chartkick, which may be common in web applications that render charts from user-supplied data. The fix added two defenses: a check for __proto__ in the extend loop and an improved isPlainObject validation that excludes functions [4].

Impact

Successful prototype pollution can lead to severe consequences, including denial of service, property injection, and in some contexts, arbitrary code execution. By polluting Object.prototype, an attacker can alter the behavior of all objects in the application, potentially bypassing security checks, modifying default settings, or triggering unanticipated code paths. The severity is elevated because Chartkick is widely used in Ruby on Rails applications for creating JavaScript charts, and the polluted prototype can affect both client-side and server-side JavaScript execution if Node.js is involved.

Mitigation

The vulnerability was patched in Chartkick gem version 3.3.0 and Chartkick.js version 3.2.0 [3][4]. Users should upgrade to Chartkick gem 3.3.0 or later, which ships with the fixed Chartkick.js. No known workarounds exist; upgrading is the recommended action. The issue is not listed in CISA's Known Exploited Vulnerabilities (KEV) catalog as of this writing.

AI Insight generated on May 21, 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
chartkickRubyGems
< 3.3.03.3.0
chartkicknpm
>= 3.1.0, < 3.2.03.2.0

Affected products

2

Patches

1
b810936bbf68

Updated Chartkick.js to 3.2.0

https://github.com/ankane/chartkickAndrew KaneNov 10, 2019via ghsa
2 files changed · +109 9
  • CHANGELOG.md+1 0 modified
    @@ -1,5 +1,6 @@
     ## 3.3.0 [unreleased]
     
    +- Updated Chartkick.js to 3.2.0
     - Rolled back Chart.js to 2.8.0 due to legend change
     
     ## 3.2.2
    
  • vendor/assets/javascripts/chartkick.js+108 9 modified
    @@ -2,15 +2,15 @@
      * Chartkick.js
      * Create beautiful charts with one line of JavaScript
      * https://github.com/ankane/chartkick.js
    - * v3.1.3
    + * v3.2.0
      * MIT License
      */
     
     (function (global, factory) {
       typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
       typeof define === 'function' && define.amd ? define(factory) :
       (global = global || self, global.Chartkick = factory());
    -}(this, function () { 'use strict';
    +}(this, (function () { 'use strict';
     
       function isArray(variable) {
         return Object.prototype.toString.call(variable) === "[object Array]";
    @@ -21,13 +21,17 @@
       }
     
       function isPlainObject(variable) {
    -    return Object.prototype.toString.call(variable) === "[object Object]";
    +    // protect against prototype pollution, defense 2
    +    return Object.prototype.toString.call(variable) === "[object Object]" && !isFunction(variable) && variable instanceof Object;
       }
     
       // https://github.com/madrobby/zepto/blob/master/src/zepto.js
       function extend(target, source) {
         var key;
         for (key in source) {
    +      // protect against prototype pollution, defense 1
    +      if (key === "__proto__") { continue; }
    +
           if (isPlainObject(source[key]) || isArray(source[key])) {
             if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
               target[key] = {};
    @@ -237,7 +241,7 @@
         return typeof obj === "number";
       }
     
    -  function formatValue(pre, value, options) {
    +  function formatValue(pre, value, options, axis) {
         pre = pre || "";
         if (options.prefix) {
           if (value < 0) {
    @@ -247,6 +251,58 @@
           pre += options.prefix;
         }
     
    +    var suffix = options.suffix || "";
    +    var precision = options.precision;
    +    var round = options.round;
    +
    +    if (options.byteScale) {
    +      var baseValue = axis ? options.byteScale : value;
    +      if (baseValue >= 1099511627776) {
    +        value /= 1099511627776;
    +        suffix = " TB";
    +      } else if (baseValue >= 1073741824) {
    +        value /= 1073741824;
    +        suffix = " GB";
    +      } else if (baseValue >= 1048576) {
    +        value /= 1048576;
    +        suffix = " MB";
    +      } else if (baseValue >= 1024) {
    +        value /= 1024;
    +        suffix = " KB";
    +      } else {
    +        suffix = " bytes";
    +      }
    +
    +      if (precision === undefined && round === undefined) {
    +        precision = 3;
    +      }
    +    }
    +
    +    if (precision !== undefined && round !== undefined) {
    +      throw Error("Use either round or precision, not both");
    +    }
    +
    +    if (!axis) {
    +      if (precision !== undefined) {
    +        value = value.toPrecision(precision);
    +        if (!options.zeros) {
    +          value = parseFloat(value);
    +        }
    +      }
    +
    +      if (round !== undefined) {
    +        if (round < 0) {
    +          var num = Math.pow(10, -1 * round);
    +          value = parseInt((1.0 * value / num).toFixed(0)) * num;
    +        } else {
    +          value = value.toFixed(round);
    +          if (!options.zeros) {
    +            value = parseFloat(value);
    +          }
    +        }
    +      }
    +    }
    +
         if (options.thousands || options.decimal) {
           value = toStr(value);
           var parts = value.split(".");
    @@ -259,7 +315,7 @@
           }
         }
     
    -    return pre + value + (options.suffix || "");
    +    return pre + value + suffix;
       }
     
       function seriesOption(chart, series, option) {
    @@ -420,18 +476,58 @@
           prefix: chart.options.prefix,
           suffix: chart.options.suffix,
           thousands: chart.options.thousands,
    -      decimal: chart.options.decimal
    +      decimal: chart.options.decimal,
    +      precision: chart.options.precision,
    +      round: chart.options.round,
    +      zeros: chart.options.zeros
         };
     
    +    if (chart.options.bytes) {
    +      var series = chart.data;
    +      if (chartType === "pie") {
    +        series = [{data: series}];
    +      }
    +
    +      // calculate max
    +      var max = 0;
    +      for (var i = 0; i < series.length; i++) {
    +        var s = series[i];
    +        for (var j = 0; j < s.data.length; j++) {
    +          if (s.data[j][1] > max) {
    +            max = s.data[j][1];
    +          }
    +        }
    +      }
    +
    +      // calculate scale
    +      var scale = 1;
    +      while (max >= 1024) {
    +        scale *= 1024;
    +        max /= 1024;
    +      }
    +
    +      // set step size
    +      formatOptions.byteScale = scale;
    +    }
    +
         if (chartType !== "pie") {
           var myAxes = options.scales.yAxes;
           if (chartType === "bar") {
             myAxes = options.scales.xAxes;
           }
     
    +      if (formatOptions.byteScale) {
    +        if (!myAxes[0].ticks.stepSize) {
    +          myAxes[0].ticks.stepSize = formatOptions.byteScale / 2;
    +        }
    +        if (!myAxes[0].ticks.maxTicksLimit) {
    +          myAxes[0].ticks.maxTicksLimit = 4;
    +        }
    +      }
    +
           if (!myAxes[0].ticks.callback) {
             myAxes[0].ticks.callback = function (value) {
    -          return formatValue("", value, formatOptions);
    +          return formatValue("", value, formatOptions, true);
             };
           }
         }
    @@ -948,7 +1044,10 @@
           prefix: chart.options.prefix,
           suffix: chart.options.suffix,
           thousands: chart.options.thousands,
    -      decimal: chart.options.decimal
    +      decimal: chart.options.decimal,
    +      precision: chart.options.precision,
    +      round: chart.options.round,
    +      zeros: chart.options.zeros
         };
     
         if (chartType !== "pie" && !options.yAxis.labels.formatter) {
    @@ -2316,4 +2415,4 @@
     
       return Chartkick;
     
    -}));
    +})));
    

Vulnerability mechanics

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

References

10

News mentions

0

No linked articles in our index yet.