CVE-2023-26133
Description
All versions of progressbar.js are vulnerable to Prototype Pollution via the extend() function in utils.js, allowing potential denial of service or remote code execution.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
All versions of progressbar.js are vulnerable to Prototype Pollution via the extend() function in utils.js, allowing potential denial of service or remote code execution.
Overview
CVE-2023-26133 describes a Prototype Pollution vulnerability in the progressbar.js package affecting all versions. The flaw resides in the extend() function within src/utils.js (line 18). The function performs an unsafe recursive merge of objects without properly filtering dangerous properties such as __proto__, constructor, or prototype, allowing an attacker to inject arbitrary properties into the global Object prototype [1][2][3].
Exploitation
Prototype Pollution occurs when the extend() function merges a source object into a target object. If the source object contains a property named __proto__, the recursive merge logic will traverse into the target's prototype chain, ultimately writing properties onto Object.prototype. An attacker can control the source object via user-supplied input, such as JSON payloads or URL parameters, depending on how the library is integrated. The attack requires no authentication and can be triggered remotely if the application processes attacker-controlled data through the vulnerable extend() call [2].
Impact
Successful exploitation can lead to a denial of service by throwing JavaScript exceptions when polluted properties interfere with regular object operations. More critically, by polluting base object properties, an attacker may alter the application's control flow, potentially leading to remote code execution (RCE) if the polluted properties affect security-sensitive code paths. The impact is application-specific but follows the typical Prototype Pollution pattern seen in libraries like lodash and Hoek [2].
Mitigation
No fixed version has been released as of the publication date. The project appears to be maintained sporadically. Developers using progressbar.js should sanitize user input before passing it to any function that internally calls extend(), or consider replacing the library with an alternative that does not perform unsafe recursive merges. The vulnerability is publicly documented and has a Snyk advisory [2].
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.
| Package | Affected versions | Patched versions |
|---|---|---|
progressbar.jsnpm | < 1.1.1 | 1.1.1 |
Affected products
2- progressbar.js/progressbar.jsdescription
Patches
197fe68ef4becUse lodash.merge over custom extend
12 files changed · +22527 −5473
Gruntfile.js+0 −16 modified@@ -1,7 +1,5 @@ -var fs = require('fs'); var _ = require('lodash'); - // Split array to smaller arrays containing n elements at max function groupToElements(array, n) { var lists = _.groupBy(array, function(element, index){ @@ -11,10 +9,6 @@ function groupToElements(array, n) { return _.toArray(lists); } -function endsWith(str, suffix) { - return str.indexOf(suffix, str.length - suffix.length) !== -1; -} - // Setup karma configuration dynamically to run browsers sequentially // https://github.com/karma-runner/karma-sauce-launcher/issues/8 var MAXIMUM_CONCURRENT_SAUCE = 3; @@ -82,13 +76,6 @@ module.exports = function(grunt) { // This will run tests in Sauce Lab command: './node_modules/karma/bin/karma start' }, - testem: { - options: { - stdout: true - }, - // This will run tests in all local browsers available/detected - command: 'testem ci -R dot -l chrome' - } }, // Uglify must be run after browserify uglify: { @@ -125,8 +112,6 @@ module.exports = function(grunt) { 'uglify:progressbar' ]); - grunt.registerTask('test', ['shell:testem']); - // Run multiple tests serially, but continue if one of them fails. // Adapted from http://stackoverflow.com/questions/16487681/gruntfile-getting-error-codes-from-programs-serially grunt.registerTask('sauce', function() { @@ -161,7 +146,6 @@ module.exports = function(grunt) { bump = bump || 'patch'; grunt.task.run([ - 'test', 'build', 'stageMinified', 'shell:release:' + bump
package.json+1 −2 modified@@ -4,13 +4,12 @@ "description": "Responsive and slick progress bars with animated SVG paths", "main": "dist/progressbar.js", "dependencies": { + "lodash.merge": "^4.6.2", "shifty": "^2.8.3" }, "devDependencies": { "bluebird": "^2.3.6", "browserify": "^13.0.0", - "chai": "^1.10.0", - "chai-stats": "kimmobrunfeldt/chai-stats", "commander": "^2.4.0", "concurrently": "^2.0.0", "eslint": "^1.0.0",
package-lock.json+22523 −4873 modifiedREADME.md+0 −7 modified@@ -21,13 +21,6 @@ Documentation is [hosted at readthedocs.org](http://progressbarjs.readthedocs.or * [Migration between versions](http://progressbarjs.readthedocs.org/en/latest/#migrations) * [react-progressbar.js](https://github.com/kimmobrunfeldt/react-progressbar.js) progress bars in React. -**Build status** - -[](https://travis-ci.org/kimmobrunfeldt/progressbar.js) *Build status and browser tests for current master* - -[](https://app.saucelabs.com/u/kimmobrunfeldt) - - # Contributing
src/utils.js+3 −23 modified@@ -1,30 +1,10 @@ // Utility functions +var merge = require('lodash.merge'); + var PREFIXES = 'Webkit Moz O ms'.split(' '); var FLOAT_COMPARISON_EPSILON = 0.001; -// Copy all attributes from source object to destination object. -// destination object is mutated. -function extend(destination, source, recursive) { - destination = destination || {}; - source = source || {}; - recursive = recursive || false; - - for (var attrName in source) { - if (source.hasOwnProperty(attrName)) { - var destVal = destination[attrName]; - var sourceVal = source[attrName]; - if (recursive && isObject(destVal) && isObject(sourceVal)) { - destination[attrName] = extend(destVal, sourceVal, recursive); - } else { - destination[attrName] = sourceVal; - } - } - } - - return destination; -} - // Renders templates with given variables. Variables must be surrounded with // braces without any spaces, e.g. {variable} // All instances of variable placeholders will be replaced with given content @@ -123,7 +103,7 @@ function removeChildren(el) { } module.exports = { - extend: extend, + extend: merge, render: render, setStyle: setStyle, setStyles: setStyles,
testem.json+0 −12 removed@@ -1,12 +0,0 @@ -{ - "framework": "mocha", - "src_files": [ - "test/test-*.js" - ], - "test_page": "test/testem.html", - "routes": { - "/node_modules": "node_modules" - }, - "before_tests": "browserify --debug test/test-all.js -o test/browserified-tests.js", - "on_exit": "rm test/browserified-tests.js" -}
test/path-behaviour.js+0 −148 removed@@ -1,148 +0,0 @@ -var chai = require('chai'); -var chaiStats = require('chai-stats'); -chai.use(chaiStats); -var expect = chai.expect; - -var PRECISION = 2; -var ANIM_PROP = { - 'styleName': 'stroke-offset', - 'scriptName': 'strokeOffset' -}; - -var barOpts = { - duration: 800, - from: {strokeOffset: 0}, - to: { strokeOffset: 0 }, - step: function(state, self, attachment) { - attachment.setAttribute(ANIM_PROP.scriptName, state[ANIM_PROP.scriptName]); - } -}; - -function createPath() { - var container = document.querySelector('body'), - svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), - path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - - svg.setAttribute('version', '1.1'); - svg.setAttribute('id', 'progress-bar'); - svg.setAttribute('x', '0px'); - svg.setAttribute('y', '0px'); - svg.setAttribute('viewBox', '0 0 197 165.39555'); - svg.setAttribute('enable-background', '0 0 197 165.39555'); - - var attrs = { - 'id': 'progress-path', - 'fill': 'none', - 'stroke': '#ccc', - 'stroke-width':'15', - 'stroke-miterlimit': '10', - 'd': 'm 31.7,160.3 c -15,-16.2 -24.2,-38 -24.2,-61.8 0,-50.3' + - ' 40.7,-91 91,-91 50.3,0 91,40.7 91,91 0,23.9 -9.2,45.6 -24.2,61.8' - }; - - for (var i in attrs) { - path.setAttribute(i, attrs[i]); - } - - svg.appendChild(path); - - container.appendChild(svg); - - return { - path: path, - svg: svg - }; -} - -function pathTests() { - - it('step function should recieve a reference to ProgressBar as argument #2', function() { - this.bar.animate(1, {duration: 500}); - - // we only care about the second arg, for each call so we need to manually - // inspect them since we dont know what state would look like - var ok = true; - - for (var i = 0; i < this.step.args.length; i++) { - if (this.step.args[i][1] !== this.bar) { - ok = false; - } - } - - expect(ok).to.be.true(); - }); - - it('step function should recieve a reference to ProgressBar as argument #3', function() { - this.bar.animate(1, {duration: 500}); - - // we only care about the third arg, for each call so we need to manually - // inspect them since we dont know what state would look like - var ok = true; - - for (var i = 0; i < this.step.args.length; i++) { - if (this.step.args[i][2] !== this.attachment) { - ok = false; - } - } - - expect(ok).to.be.true(); - }); - - it('set should change value', function() { - this.bar.set(1); - expect(this.bar.value()).to.almost.equal(1, PRECISION); - }); - - it('animate should change SVG path stroke-dashoffset property', function(done) { - var progressAtStart = this.bar.value(); - this.bar.animate(1, {duration: 1000}); - - var self = this; - setTimeout(function checkOffsetHasChanged() { - expect(self.bar.value()).to.be.greaterThan(progressAtStart); - expect(self.bar.value()).to.be.lessThan(1); - done(); - }, 100); - }); - - it('animate should change value', function(done) { - this.bar.set(1); - this.bar.animate(0, {duration: 600}); - - var self = this; - setTimeout(function checkValueHasChanged() { - expect(self.bar.value()).not.to.almost.equal(1, PRECISION); - }, 300); - - setTimeout(function checkAnimationHasCompleted() { - expect(self.bar.value()).to.almost.equal(0, PRECISION); - done(); - }, 1200); - }); - - it('stop() should stop animation', function(done) { - this.bar.animate(1, {duration: 1000}); - - var self = this; - var progressAfterStop; - setTimeout(function stopAnimation() { - self.bar.stop(); - progressAfterStop = self.bar.value(); - }, 100); - - setTimeout(function checkProgressAfterStop() { - expect(progressAfterStop).to.almost.equal(self.bar.value(), PRECISION); - done(); - }, 400); - }); - - it('.path attribute should exist', function() { - expect(this.bar.path).to.be.ok(); - }); -} - -module.exports = { - options: barOpts, - runTests: pathTests, - createPath: createPath -};
test/shape-behaviour.js+0 −175 removed@@ -1,175 +0,0 @@ -// Tests which test shared behaviour of all progress bar shapes - -var chai = require('chai'); -var chaiStats = require('chai-stats'); -chai.use(chaiStats); -var expect = chai.expect; - -var PRECISION = 2; -var TEXT_CLASS_NAME = '.progressbar-text'; - -var sharedTests = function sharedTests() { - // Test that public attributes exist - it('.svg attribute should exist', function() { - expect(this.bar.svg).to.be.ok(); - }); - - it('.path attribute should exist', function() { - expect(this.bar.path).to.be.ok(); - }); - - it('.trail attribute should exist', function() { - expect(this.bar.trail).to.be.ok(); - }); - - it('.text attribute should exist', function() { - expect(this.bar.text).to.be.ok(); - }); - - it('bar should be empty after initialization', function() { - expect(this.bar.value()).to.almost.equal(0, PRECISION); - }); - - it('set should change value', function() { - this.bar.set(1); - expect(this.bar.value()).to.almost.equal(1, PRECISION); - }); - - it('animate should change SVG path stroke-dashoffset property', function(done) { - var progressAtStart = this.bar.value(); - this.bar.animate(1, {duration: 1000}); - - var self = this; - setTimeout(function checkOffsetHasChanged() { - expect(self.bar.value()).to.be.greaterThan(progressAtStart); - expect(self.bar.value()).to.be.lessThan(1); - done(); - }, 100); - }); - - it('animate should change value', function(done) { - this.bar.set(1); - this.bar.animate(0, {duration: 600}); - - var self = this; - setTimeout(function checkValueHasChanged() { - expect(self.bar.value()).not.to.almost.equal(1, PRECISION); - }, 300); - - setTimeout(function checkAnimationHasCompleted() { - expect(self.bar.value()).to.almost.equal(0, PRECISION); - done(); - }, 1200); - }); - - it('step function should recieve a reference to ProgressBar as argument #2', function() { - this.bar.animate(1, {duration: 600}); - var allCallsHaveBar = true; - - for (var i = 0; i < this.step.args.length; i++) { - if (this.step.args[i][1] !== this.bar) { - allCallsHaveBar = false; - } - } - - expect(allCallsHaveBar).to.be.true(); - }); - - it('step function should recieve a reference to attachment as argument #3', function() { - this.bar.animate(1, {duration: 600}); - var allCallsHaveAttachment = true; - - for (var i = 0; i < this.step.args.length; i++) { - if (this.step.args[i][2] !== this.attachment) { - allCallsHaveAttachment = false; - } - } - - expect(allCallsHaveAttachment).to.be.true(); - }); - - it('stop() should stop animation', function(done) { - this.bar.animate(1, {duration: 1000}); - - var self = this; - var progressAfterStop; - setTimeout(function stopAnimation() { - self.bar.stop(); - progressAfterStop = self.bar.value(); - }, 100); - - setTimeout(function checkProgressAfterStop() { - expect(progressAfterStop).to.almost.equal(self.bar.value(), PRECISION); - done(); - }, 400); - }); - - // We have to test these two functions together - it('pause() & resume() should pause & resume animation', function(done) { - this.bar.animate(1, {duration: 1000}); - - var self = this; - var progressAfterPause; - - setTimeout(function pauseAnimation() { - self.bar.pause(); - progressAfterPause = self.bar.value(); - }, 100); - - setTimeout(function checkProgressAfterPause() { - expect(progressAfterPause).to.almost.equal(self.bar.value(), PRECISION); - }, 400); - - setTimeout(function resumeAnimation() { - self.bar.resume(); - setTimeout(function checkProgressAfterResume() { - // Make sure it did resume quickly (<60ms) - expect(self.bar.value() > progressAfterPause + 0.14).to.be.true; - done(); - }, 200); - }, 600); - }); - - it('destroy() should delete DOM elements', function() { - var svg = document.querySelector('svg'); - expect(svg).not.to.equal(null); - - var textElement = document.querySelector(TEXT_CLASS_NAME); - expect(textElement).not.to.equal(null); - - this.bar.destroy(); - svg = document.querySelector('svg'); - expect(svg).to.equal(null); - - textElement = document.querySelector(TEXT_CLASS_NAME); - expect(textElement).to.equal(null); - }); - - it('destroy() should make object unusable', function() { - this.bar.destroy(); - - var self = this; - var methodsShouldThrow = ['destroy', 'value', 'set', 'animate', 'stop', 'setText']; - methodsShouldThrow.forEach(function(methodName) { - expect(function shouldThrow() { - self[methodName](); - }).to.throw(Error); - }); - - expect(this.bar.svg).to.equal(null); - expect(this.bar.path).to.equal(null); - expect(this.bar.trail).to.equal(null); - expect(this.bar.text).to.equal(null); - }); - - it('setText() should change text element', function() { - var textElement = document.querySelector(TEXT_CLASS_NAME); - expect(textElement.textContent).to.equal('Test'); - this.bar.setText('new'); - - textElement = document.querySelector(TEXT_CLASS_NAME); - expect(textElement.textContent).to.equal('new'); - }); -}; - -module.exports = sharedTests;
test/test-all.js+0 −172 removed@@ -1,172 +0,0 @@ -// These tests are run with two different test runners which work a bit -// differently. Runners are Testem and Karma. Read more about them in -// CONTRIBUTING.md -// Supporting both has lead to some compromises. - -var chai = require('chai'); -var chaiStats = require('chai-stats'); -chai.use(chaiStats); -var sinon = require('sinon'); -var expect = chai.expect; - -// https://github.com/mochajs/mocha/wiki/Shared-Behaviours -var shapeTests = require('./shape-behaviour'); -var pathTests = require('./path-behaviour'); -var ProgressBar = require('../src/main'); -var utils = require('../src/utils'); - -var afterEachCase = function() { - try { - this.bar.destroy(); - } catch (e) { - // Some test cases destroy the bar themselves and calling again - // throws an error - } -}; - -var barOpts = { - text: { value: 'Test' }, - trailWidth: 1, - attachment: {foo: 'bar'} -}; - -describe('Line', function() { - beforeEach(function() { - // Append progress bar to body since adding a custom HTML and div - // with Karma was not that trivial compared to Testem - barOpts.step = function(state, bar, attachment) {}; - this.bar = new ProgressBar.Line('body', barOpts); - this.attachment = this.bar._opts.attachment; - this.step = sinon.spy(this.bar._opts, 'step'); - - }); - - afterEach(afterEachCase); - shapeTests(); -}); - -describe('Circle', function() { - beforeEach(function() { - barOpts.step = function(state, bar, attachment) {}; - this.bar = new ProgressBar.Circle('body', barOpts); - this.attachment = this.bar._opts.attachment; - this.step = sinon.spy(this.bar._opts, 'step'); - - }); - - afterEach(afterEachCase); - shapeTests(); -}); - -describe('SemiCircle', function() { - beforeEach(function() { - barOpts.step = function(state, bar, attachment) {}; - this.bar = new ProgressBar.SemiCircle('body', barOpts); - this.attachment = this.bar._opts.attachment; - this.step = sinon.spy(this.bar._opts, 'step'); - }); - - afterEach(afterEachCase); - shapeTests(); -}); - -describe('Square', function() { - beforeEach(function() { - barOpts.step = function(state, bar, attachment) {}; - this.bar = new ProgressBar.Square('body', barOpts); - this.attachment = this.bar._opts.attachment; - this.step = sinon.spy(this.bar._opts, 'step'); - }); - - afterEach(afterEachCase); - shapeTests(); -}); - -describe('Path', function() { - - beforeEach(function() { - var svgView = pathTests.createPath(); - this.svg = svgView.svg; - this.path = svgView.path; - this.path.setAttribute('strokeOffset', this.path.getTotalLength()); - pathTests.options.attachment = this.path; - - this.bar = new ProgressBar.Path(this.path, pathTests.options); - this.attachment = this.bar._opts.attachment; - this.step = sinon.spy(this.bar._opts, 'step'); - }); - - afterEach(function() { - var container = this.svg.parentNode; - container.removeChild(this.svg); - this.svg = null; - pathTests.options.attachment = null; - this.path = null; - this.bar = null; - }); - - pathTests.runTests(); -}); - -describe('utils', function() { - it('extend without recursive should not merge', function() { - var first = { - a: { content: 1 }, - b: 2, - c: 3, - d: [1, 2] - - }; - var second = { - a: { test: 1 }, - b: 4, - d: [], - e: 1 - }; - utils.extend(first, second); - - // These should normally override a's attributes - expect(first.a.content).to.equal(undefined); - expect(first.b).to.equal(second.b); - expect(first.d).to.equal(second.d); - expect(first.e).to.equal(second.e); - - // b.c is undefined so c should not be modified - expect(first.c).to.equal(first.c); - }); - - it('extend with recursive should merge', function() { - var first = { - a: { - content: 1, - b: { - content: 2 - } - }, - arr: [1, 2] - }; - var second = { - a: { - test: 1, - b: { - test: 2 - } - }, - arr: [] - }; - utils.extend(first, second, true); - - // These should normally override a's attributes - expect(first).to.deep.equal({ - a: { - content: 1, - test: 1, - b: { - content: 2, - test: 2 - } - }, - arr: [] - }); - }); -});
test/testem.html+0 −34 removed@@ -1,34 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <meta charset="utf-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> - <title>Test</title> - <meta name="viewport" content="width=device-width, initial-scale=1"> - - <link rel="stylesheet" href="/node_modules/mocha/mocha.css" /> - </head> - <body> - <div id="mocha"></div> - - <!-- Container where progress bars are appended--> - - <div id="test-container"> - <h1>Test div</h1> - <div id="container"></div> - </div> - - <script crossorigin="anonymous" src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> - <script src="/node_modules/mocha/mocha.js"></script> - <script>mocha.setup('bdd')</script> - <script src="/testem.js"></script> - - <!-- Tests --> - <script src="browserified-tests.js"></script> - - <script> - mocha.checkLeaks(); - mocha.run(); - </script> - </body> -</html>
test/test-utils.js+0 −8 removed@@ -1,8 +0,0 @@ -function getComputedStyle(element, property) { - var computedStyle = window.getComputedStyle(element, null); - return computedStyle.getPropertyValue(property); -} - -module.exports = { - getComputedStyle: getComputedStyle -};
tools/test.sh+0 −3 modified@@ -8,7 +8,4 @@ EXIT_STATUS=0 echo -e "\n---- Linting code..\n" npm run lint || EXIT_STATUS=$? -echo -e "\n---- Running tests..\n" -grunt test || EXIT_STATUS=$? - exit $EXIT_STATUS
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-89qm-hm2x-mxm3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-26133ghsaADVISORY
- github.com/kimmobrunfeldt/progressbar.js/blob/74536b9eeeaaf51144706d918ed5a0a679631d96/src/utils.jsghsaWEB
- github.com/kimmobrunfeldt/progressbar.js/blob/74536b9eeeaaf51144706d918ed5a0a679631d96/src/utils.jsghsaWEB
- github.com/kimmobrunfeldt/progressbar.js/commit/97fe68ef4beccfe84b7cba08ea1fc695e38cc04bghsaWEB
- security.snyk.io/vuln/SNYK-JS-PROGRESSBARJS-3184152ghsaWEB
- github.com/kimmobrunfeldt/progressbar.js/blob/74536b9eeeaaf51144706d918ed5a0a679631d96/src/utils.js%23L18mitre
- github.com/kimmobrunfeldt/progressbar.js/blob/74536b9eeeaaf51144706d918ed5a0a679631d96/src/utils.js%23L20mitre
News mentions
0No linked articles in our index yet.