Skip to content

Commit

Permalink
[New] parse: add strictDepth option
Browse files Browse the repository at this point in the history
throw tests, readme update
  • Loading branch information
jonchurch committed Aug 1, 2024
1 parent c9a6694 commit 8d56df2
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 2 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,18 @@ var deep = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
assert.deepEqual(deep, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } });
```

The depth limit helps mitigate abuse when **qs** is used to parse user input, and it is recommended to keep it a reasonably small number.
You can configure **qs** to throw an error when parsing nested input beyond this depth using the `strictDepth` option (defaulted to false):

```javascript
try {
qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
} catch (err) {
assert(err instanceof RangeError);
assert.strictEqual(err.message, 'Input depth exceeded depth option of 1 and strictDepth is true');
}
```

The depth limit helps mitigate abuse when **qs** is used to parse user input, and it is recommended to keep it a reasonably small number. The strictDepth option adds a layer of protection by throwing an error when the limit is exceeded, allowing you to catch and handle such cases.

For similar reasons, by default **qs** will only parse up to 1000 parameters. This can be overridden by passing a `parameterLimit` option:

Expand Down
7 changes: 6 additions & 1 deletion lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var defaults = {
parameterLimit: 1000,
parseArrays: true,
plainObjects: false,
strictDepth: false,
strictNullHandling: false
};

Expand Down Expand Up @@ -201,9 +202,12 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesPars
keys.push(segment[1]);
}

// If there's a remainder, just add whatever is left
// If there's a remainder, check strictDepth option for throw, else just add whatever is left

if (segment) {
if (options.strictDepth === true) {
throw new RangeError('Input depth exceeded depth option of ' + options.depth + ' and strictDepth is true');
}
keys.push('[' + key.slice(segment.index) + ']');
}

Expand Down Expand Up @@ -260,6 +264,7 @@ var normalizeParseOptions = function normalizeParseOptions(opts) {
parameterLimit: typeof opts.parameterLimit === 'number' ? opts.parameterLimit : defaults.parameterLimit,
parseArrays: opts.parseArrays !== false,
plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects,
strictDepth: typeof opts.strictDepth === 'boolean' ? !!opts.strictDepth : defaults.strictDepth,
strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling
};
};
Expand Down
100 changes: 100 additions & 0 deletions test/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -1068,3 +1068,103 @@ test('`duplicates` option', function (t) {

t.end();
});

test('qs strictDepth option - throw cases', function (t) {
t.test('throws an exception when depth exceeds the limit with strictDepth: true', function (st) {
st['throws'](
function () {
qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
},
RangeError,
'Should throw RangeError'
);
st.end();
});

t.test('throws an exception for multiple nested arrays with strictDepth: true', function (st) {
st['throws'](
function () {
qs.parse('a[0][1][2][3][4]=b', { depth: 3, strictDepth: true });
},
RangeError,
'Should throw RangeError'
);
st.end();
});

t.test('throws an exception for nested objects and arrays with strictDepth: true', function (st) {
st['throws'](
function () {
qs.parse('a[b][c][0][d][e]=f', { depth: 3, strictDepth: true });
},
RangeError,
'Should throw RangeError'
);
st.end();
});

t.test('throws an exception for different types of values with strictDepth: true', function (st) {
st['throws'](
function () {
qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 3, strictDepth: true });
},
RangeError,
'Should throw RangeError'
);
st.end();
});

});

test('qs strictDepth option - non-throw cases', function (t) {
t.test('when depth is 0 and strictDepth true, do not throw', function (st) {
st.doesNotThrow(
function () {
qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 0, strictDepth: true });
},
RangeError,
'Should not throw RangeError'
);
st.end();
});

t.test('parses successfully when depth is within the limit with strictDepth: true', function (st) {
st.doesNotThrow(
function () {
var result = qs.parse('a[b]=c', { depth: 1, strictDepth: true });
st.deepEqual(result, { a: { b: 'c' } }, 'Should parse correctly');
}
);
st.end();
});

t.test('does not throw an exception when depth exceeds the limit with strictDepth: false', function (st) {
st.doesNotThrow(
function () {
var result = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
st.deepEqual(result, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }, 'Should parse with depth limit');
}
);
st.end();
});

t.test('parses successfully when depth is within the limit with strictDepth: false', function (st) {
st.doesNotThrow(
function () {
var result = qs.parse('a[b]=c', { depth: 1 });
st.deepEqual(result, { a: { b: 'c' } }, 'Should parse correctly');
}
);
st.end();
});

t.test('does not throw when depth is exactly at the limit with strictDepth: true', function (st) {
st.doesNotThrow(
function () {
var result = qs.parse('a[b][c]=d', { depth: 2, strictDepth: true });
st.deepEqual(result, { a: { b: { c: 'd' } } }, 'Should parse correctly');
}
);
st.end();
});
});

0 comments on commit 8d56df2

Please sign in to comment.