diff --git a/.eslintrc b/.eslintrc index e448a2f5..2c680d75 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,7 +10,7 @@ "id-length": [2, { "min": 1, "max": 25, "properties": "never" }], "indent": [2, 4], "max-lines-per-function": [2, { "max": 150 }], - "max-params": [2, 14], + "max-params": [2, 15], "max-statements": [2, 52], "multiline-comment-style": 0, "no-continue": 1, diff --git a/.github/workflows/node-zero.yml b/.github/workflows/node-zero.yml index 4e72a688..6f18484c 100644 --- a/.github/workflows/node-zero.yml +++ b/.github/workflows/node-zero.yml @@ -52,7 +52,11 @@ jobs: cache-node-modules-key: node_modules-${{ github.workflow }}-${{ github.action }}-${{ github.run_id }} skip-ls-check: true - run: npm run tests-only + if: ${{ matrix.node-version }} != 0.6 + - run: ./node_modules/.bin/tape 'test/**/*.js' + if: ${{ matrix.node-version }} = 0.6 - run: bash <(curl -s https://codecov.io/bash) -f coverage/*.json; + if: ${{ matrix.node-version }} != 0.6 node: name: 'node 0.x' diff --git a/lib/stringify.js b/lib/stringify.js index f46bb0e1..ab7a37e2 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -1,5 +1,6 @@ 'use strict'; +var getSideChannel = require('side-channel'); var utils = require('./utils'); var formats = require('./formats'); var has = Object.prototype.hasOwnProperty; @@ -68,9 +69,15 @@ var stringify = function stringify( format, formatter, encodeValuesOnly, - charset + charset, + sideChannel ) { var obj = object; + + if (sideChannel.has(object)) { + throw new RangeError('Cyclic object value'); + } + if (typeof filter === 'function') { obj = filter(prefix, obj); } else if (obj instanceof Date) { @@ -129,6 +136,7 @@ var stringify = function stringify( ? typeof generateArrayPrefix === 'function' ? generateArrayPrefix(prefix, key) : prefix : prefix + (allowDots ? '.' + key : '[' + key + ']'); + sideChannel.set(object, true); pushToArray(values, stringify( value, keyPrefix, @@ -143,7 +151,8 @@ var stringify = function stringify( format, formatter, encodeValuesOnly, - charset + charset, + sideChannel )); } @@ -237,6 +246,7 @@ module.exports = function (object, opts) { objKeys.sort(options.sort); } + var sideChannel = getSideChannel(); for (var i = 0; i < objKeys.length; ++i) { var key = objKeys[i]; @@ -257,7 +267,8 @@ module.exports = function (object, opts) { options.format, options.formatter, options.encodeValuesOnly, - options.charset + options.charset, + sideChannel )); } diff --git a/package.json b/package.json index cb9c122e..d6fc93b8 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,9 @@ "engines": { "node": ">=0.6" }, + "dependencies": { + "side-channel": "^1.0.4" + }, "devDependencies": { "@ljharb/eslint-config": "^17.5.1", "aud": "^1.1.4", @@ -55,7 +58,7 @@ "tests-only": "nyc tape 'test/**/*.js'", "posttest": "aud --production", "readme": "evalmd README.md", - "postlint": "eclint check * lib/* test/*", + "postlint": "eclint check * lib/* test/* !dist/*", "lint": "eslint lib/*.js test/*.js", "dist": "mkdirp dist && browserify --standalone Qs lib/index.js > dist/qs.js" }, diff --git a/test/stringify.js b/test/stringify.js index 59324e0c..b2b3f4b5 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -433,7 +433,7 @@ test('stringify()', function (t) { st.end(); }); - t.test('doesn\'t blow up when Buffer global is missing', function (st) { + t.test('does not blow up when Buffer global is missing', function (st) { var tempBuffer = global.Buffer; delete global.Buffer; var result = qs.stringify({ a: 'b', c: 'd' }); @@ -442,6 +442,29 @@ test('stringify()', function (t) { st.end(); }); + t.test('does not crash when parsing circular references', function (st) { + var a = {}; + a.b = a; + + st['throws']( + function () { qs.stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); }, + RangeError, + 'cyclic values throw' + ); + + var circular = { + a: 'value' + }; + circular.a = circular; + st['throws']( + function () { qs.stringify(circular); }, + RangeError, + 'cyclic values throw' + ); + + st.end(); + }); + t.test('selects properties when filter=array', function (st) { st.equal(qs.stringify({ a: 'b' }, { filter: ['a'] }), 'a=b'); st.equal(qs.stringify({ a: 1 }, { filter: [] }), '');