CVE-2021-41097 (published 2021-09-27T18:15Z)

aurelia-path is part of the Aurelia platform and contains utilities for path manipulation. There is a prototype pollution vulnerability in aurelia-path before version 1.1.7. The vulnerability exposes Aurelia application that uses `aurelia-path` package to parse a string. The majority of this will be Aurelia applications that employ the `aurelia-router` package. An example is this could allow an attacker to change the prototype of base object class `Object` by tricking an application to parse the following URL: `https://aurelia.io/blog/?__proto__[asdf]=asdf`. The problem is patched in version `1.1.7`.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-1321
cwe_name: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
description: The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
extended_description: 
				By adding or modifying attributes of an object prototype, it is possible to create attributes that exist on every object, or replace critical attributes with malicious ones. This can be problematic if the product depends on existence or non-existence of certain attributes, or uses pre-defined attributes of object prototype (such as hasOwnProperty, toString or valueOf).
				This weakness is usually exploited by using a special attribute of objects called proto,  constructor or prototype. Such attributes give access to the object prototype. This weakness is often found in code that assigns object attributes based on user input, or merges or clones objects recursively.
			
			

Fix information:
repo_url: https://github.com/aurelia/path
commit_details: repo_url: https://github.com/aurelia/path
author: bigopon
author_date: 2021-06-09 16:30:20+10:00
msg: fix: security issue gh closes #44
repository: repo_url: https://github.com/aurelia/path
repo_name: aurelia/path
description: Utilities for path manipulation.
repo_language: TypeScript
owner: aurelia
file_changes: filename: index.ts
change_type: ModificationType.RENAME
diff: @@ -158,7 +158,7 @@ function buildParam(key: string, value: any, traditional?: boolean): Array<strin
 * @param traditional Boolean Use the old URI template standard (RFC6570)
 * @returns The generated query string, excluding leading '?'.
 */
-export function buildQueryString(params: Object, traditional?: Boolean): string {
+export function buildQueryString(params?: Object, traditional?: boolean): string {
   let pairs = [];
   let keys = Object.keys(params || {}).sort();
   for (let i = 0, len = keys.length; i < len; i++) {
@@ -203,16 +203,19 @@ function processScalarParam(existedParam: Object, value: Object): Object {
 * @param keys Collection of keys related to this parameter.
 * @param value Parameter value to append.
 */
-function parseComplexParam(queryParams: Object, keys: Object, value: any): void {
+function parseComplexParam(queryParams: Object, keys: (string | number)[], value: any): void {
   let currentParams = queryParams;
   let keysLastIndex = keys.length - 1;
   for (let j = 0; j <= keysLastIndex; j++) {
-    let key = keys[j] === '' ? currentParams.length : keys[j];
+    let key = keys[j] === '' ? (currentParams as any).length : keys[j];
+    if (key === '__proto__') {
+      throw new Error('Prototype pollution detected.');
+    }
     if (j < keysLastIndex) {
       // The value has to be an array or a false value
       // It can happen that the value is no array if the key was repeated with traditional style like `list=1&list[]=2`
       let prevValue = !currentParams[key] || typeof currentParams[key] === 'object' ? currentParams[key] : [currentParams[key]];
-      currentParams = currentParams[key] = prevValue || (isNaN(keys[j + 1]) ? {} : []);
+      currentParams = currentParams[key] = prevValue || (isNaN(keys[j + 1] as number) ? {} : []);
     } else {
       currentParams = currentParams[key] = value;
     }

programming_language: TypeScript
filename: path.spec.js
change_type: ModificationType.DELETE
diff: @@ -1,311 +0,0 @@
-import { relativeToFile, join, parseQueryString, buildQueryString } from '../src/index';
-
-describe('relativeToFile', () => {
-  it('can make a dot path relative to a simple file', () => {
-    var file = 'some/file.html';
-    var path = './other/module';
-
-    expect(relativeToFile(path, file)).toBe('some/other/module');
-  });
-
-  it('can make a dot path relative to an absolute file', () => {
-    var file = 'http://durandal.io/some/file.html';
-    var path = './other/module';
-
-    expect(relativeToFile(path, file)).toBe('http://durandal.io/some/other/module');
-  });
-
-  it('can make a double dot path relative to an absolute file', () => {
-    var file = 'http://durandal.io/some/file.html';
-    var path = '../other/module';
-
-    expect(relativeToFile(path, file)).toBe('http://durandal.io/other/module');
-  });
-
-  it('returns path if null file provided', () => {
-    var file = null;
-    var path = 'module';
-
-    expect(relativeToFile(path, file)).toBe('module');
-  });
-
-  it('returns path if empty file provided', () => {
-    var file = '';
-    var path = 'module';
-
-    expect(relativeToFile(path, file)).toBe('module');
-  });
-});
-
-describe('join', () => {
-  it('can combine two simple paths', () => {
-    var path1 = 'one';
-    var path2 = 'two';
-
-    expect(join(path1, path2)).toBe('one/two');
-  });
-
-  it('can combine an absolute path and a simple path', () => {
-    var path1 = '/one';
-    var path2 = 'two';
-
-    expect(join(path1, path2)).toBe('/one/two');
-  });
-
-  it('can combine an absolute path and a simple path with slash', () => {
-    var path1 = '/one';
-    var path2 = '/two';
-
-    expect(join(path1, path2)).toBe('/one/two');
-  });
-
-  it('can combine a single slash and a simple path', () => {
-    var path1 = '/';
-    var path2 = 'two';
-
-    expect(join(path1, path2)).toBe('/two');
-  });
-
-  it('can combine a single slash and a simple path with slash', () => {
-    var path1 = '/';
-    var path2 = '/two';
-
-    expect(join(path1, path2)).toBe('/two');
-  });
-
-  it('can combine an absolute path with protocol and a simple path', () => {
-    var path1 = 'http://durandal.io';
-    var path2 = 'two';
-
-    expect(join(path1, path2)).toBe('http://durandal.io/two');
-  });
-
-  it('can combine an absolute path with protocol and a simple path with slash', () => {
-    var path1 = 'http://durandal.io';
-    var path2 = '/two';
-
-    expect(join(path1, path2)).toBe('http://durandal.io/two');
-  });
-
-  it('can combine an absolute path and a simple path with a dot', () => {
-    var path1 = 'http://durandal.io';
-    var path2 = './two';
-
-    expect(join(path1, path2)).toBe('http://durandal.io/two');
-  });
-
-  it('can combine a simple path and a relative path', () => {
-    var path1 = 'one';
-    var path2 = '../two';
-
-    expect(join(path1, path2)).toBe('two');
-  });
-
-  it('can combine an absolute path and a relative path', () => {
-    var path1 = 'http://durandal.io/somewhere';
-    var path2 = '../two';
-
-    expect(join(path1, path2)).toBe('http://durandal.io/two');
-  });
-
-  it('can combine a protocol independent path and a simple path', () => {
-    var path1 = '//durandal.io';
-    var path2 = 'two';
-
-    expect(join(path1, path2)).toBe('//durandal.io/two');
-  });
-
-  it('can combine a protocol independent path and a simple path with slash', () => {
-    var path1 = '//durandal.io';
-    var path2 = '/two';
-
-    expect(join(path1, path2)).toBe('//durandal.io/two');
-  });
-
-  it('can combine a protocol independent path and a simple path with a dot', () => {
-    var path1 = '//durandal.io';
-    var path2 = './two';
-
-    expect(join(path1, path2)).toBe('//durandal.io/two');
-  });
-
-  it('can combine a protocol independent path and a relative path', () => {
-    var path1 = '//durandal.io/somewhere';
-    var path2 = '../two';
-
-    expect(join(path1, path2)).toBe('//durandal.io/two');
-  });
-
-  it('can combine a complex path and a relative path', () => {
-    var path1 = 'one/three';
-    var path2 = '../two';
-
-    expect(join(path1, path2)).toBe('one/two');
-  });
-
-  it('returns path2 if path1 null', () => {
-    var path1 = null;
-    var path2 = 'two';
-
-    expect(join(path1, path2)).toBe('two');
-  });
-
-  it('returns path2 if path1 empty', () => {
-    var path1 = '';
-    var path2 = 'two';
-
-    expect(join(path1, path2)).toBe('two');
-  });
-
-  it('returns path1 if path2 null', () => {
-    var path1 = 'one';
-    var path2 = null;
-
-    expect(join(path1, path2)).toBe('one');
-  });
-
-  it('returns path1 if path2 empty', () => {
-    var path1 = 'one';
-    var path2 = '';
-
-    expect(join(path1, path2)).toBe('one');
-  });
-
-  it('should retain leading .. in path1', () => {
-    var path1 = '../one';
-    var path2 = './two';
-
-    expect(join(path1, path2)).toBe('../one/two');
-  });
-
-  it('should retain consecutive leading .. in path1', () => {
-    var path1 = '../../one';
-    var path2 = './two';
-
-    expect(join(path1, path2)).toBe('../../one/two');
-  });
-
-  it('should handle .. in path1 and path2', () => {
-    var path1 = '../../one';
-    var path2 = '../two';
-
-    expect(join(path1, path2)).toBe('../../two');
-  });
-
-  it('should merge .. in path1 and path2', () => {
-    var path1 = '../../one';
-    var path2 = '../../two';
-
-    expect(join(path1, path2)).toBe('../../../two');
-  });
-
-  it('should retain consecutive leading .. but not other .. in path1', () => {
-    var path1 = '../../one/../three';
-    var path2 = './two';
-
-    expect(join(path1, path2)).toBe('../../three/two');
-  });
-
-  it('should respect a trailing slash', () => {
-    var path1 = 'one/';
-    var path2 = 'two/';
-
-    expect(join(path1, path2)).toBe('one/two/');
-  });
-
-  it('should respect file:/// protocol with three slashes (empty host)', () => {
-    var path1 = 'file:///one';
-    var path2 = '/two';
-
-    expect(join(path1, path2)).toBe('file:///one/two');
-  });
-
-  it('should respect file:// protocol with two slashes (host given)', () => {
-    var path1 = 'file://localhost:8080';
-    var path2 = '/two';
-
-    expect(join(path1, path2)).toBe('file://localhost:8080/two');
-  });
-
-  it('should allow scheme-relative URL that uses colons in the path', () => {
-    var path1 = '//localhost/one:/';
-    var path2 = '/two';
-
-    expect(join(path1, path2)).toBe('//localhost/one:/two');
-  });
-
-  it('should not add more than two leading slashes to http:// protocol', () => {
-    var path1 = 'http:///';
-    var path2 = '/two';
-
-    expect(join(path1, path2)).toBe('http://two');
-  });
-});
-
-describe('query strings', () => {
-  it('should build query strings', () => {
-    let gen = buildQueryString;
-
-    expect(gen()).toBe('');
-    expect(gen(null)).toBe('');
-    expect(gen({})).toBe('');
-    expect(gen({ a: null })).toBe('');
-
-    expect(gen({ '': 'a' })).toBe('=a');
-    expect(gen({ a: 'b' })).toBe('a=b');
-    expect(gen({ a: 'b', c: 'd' })).toBe('a=b&c=d');
-    expect(gen({ a: 'b', c: 'd' }, true)).toBe('a=b&c=d');
-    expect(gen({ a: 'b', c: null })).toBe('a=b');
-    expect(gen({ a: 'b', c: null }, true)).toBe('a=b');
-
-    expect(gen({ a: ['b', 'c'] })).toBe('a%5B%5D=b&a%5B%5D=c');
-    expect(gen({ a: ['b', 'c'] }, true)).toBe('a=b&a=c');
-    expect(gen({ '&': ['b', 'c'] })).toBe('%26%5B%5D=b&%26%5B%5D=c');
-    expect(gen({ '&': ['b', 'c'] }, true)).toBe('%26=b&%26=c');
-
-    expect(gen({ a: '&' })).toBe('a=%26');
-    expect(gen({ '&': 'a' })).toBe('%26=a');
-    expect(gen({ a: true })).toBe('a=true');
-    expect(gen({ '$test': true })).toBe('$test=true');
-
-    expect(gen({ obj: { a: 5, b: "str", c: false } })).toBe('obj%5Ba%5D=5&obj%5Bb%5D=str&obj%5Bc%5D=false');
-    expect(gen({ obj: { a: 5, b: "str", c: false } }, true)).toBe('obj=%5Bobject%20Object%5D');
-    expect(gen({ obj:{ a: 5, b: undefined}})).toBe('obj%5Ba%5D=5');
-
-    expect(gen({a: {b: ['c','d', ['f', 'g']]}})).toBe('a%5Bb%5D%5B%5D=c&a%5Bb%5D%5B%5D=d&a%5Bb%5D%5B2%5D%5B%5D=f&a%5Bb%5D%5B2%5D%5B%5D=g');
-    expect(gen({a: {b: ['c','d', ['f', 'g']]}}, true)).toBe('a=%5Bobject%20Object%5D');
-    expect(gen({a: ['c','d', ['f', 'g']]}, true)).toBe('a=c&a=d&a=f%2Cg');
-    expect(gen({a: ['c','d', {f: 'g'}]}, true)).toBe('a=c&a=d&a=%5Bobject%20Object%5D');
-  });
-
-  it('should parse query strings', () => {
-    let parse = parseQueryString;
-
-    expect(parse('')).toEqual({});
-    expect(parse('=')).toEqual({});
-    expect(parse('&')).toEqual({});
-    expect(parse('?')).toEqual({});
-
-    expect(parse('a')).toEqual({ a: true });
-    expect(parse('a&b')).toEqual({ a: true, b: true });
-    expect(parse('a=')).toEqual({ a: '' });
-    expect(parse('a=&b=')).toEqual({ a: '', b: '' });
-
-    expect(parse('a=b')).toEqual({ a: 'b' });
-    expect(parse('a=b&c=d')).toEqual({ a: 'b', c: 'd' });
-    expect(parse('a=b&&c=d')).toEqual({ a: 'b', c: 'd' });
-    expect(parse('a=b&a=c')).toEqual({ a: ['b', 'c'] });
-
-    expect(parse('a=b&c=d=')).toEqual({ a: 'b', c: 'd' });
-    expect(parse('a=b&c=d==')).toEqual({ a: 'b', c: 'd' });
-
-    expect(parse('a=%26')).toEqual({ a: '&' });
-    expect(parse('%26=a')).toEqual({ '&': 'a' });
-    expect(parse('%26[]=b&%26[]=c')).toEqual({ '&': ['b', 'c'] });
-
-    expect(parse('a[b]=c&a[d]=e')).toEqual({a: {b: 'c', d: 'e'}});
-    expect(parse('a[b][c][d]=e')).toEqual({a: {b: {c: {d: 'e'}}}});
-    expect(parse('a[b][]=c&a[b][]=d&a[b][2][]=f&a[b][2][]=g')).toEqual({a: {b: ['c','d', ['f', 'g']]}});
-    expect(parse('a[0]=b')).toEqual({a: ['b']});
-  });
-});

programming_language: unknown
filename: path.spec.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,344 @@
+import { relativeToFile, join, parseQueryString, buildQueryString } from '../src/index';
+import assert from 'assert';
+
+const expect = (input: any) => {
+  return {
+    toBe: (output: any) => {
+      assert.strictEqual(input, output);
+    },
+    toEqual: (output: any) => {
+      assert.deepStrictEqual(input, output)
+    }
+  }
+}
+
+describe('relativeToFile', () => {
+  it('can make a dot path relative to a simple file', () => {
+    const file = 'some/file.html';
+    const path = './other/module';
+
+    expect(relativeToFile(path, file)).toBe('some/other/module');
+  });
+
+  it('can make a dot path relative to an absolute file', () => {
+    const file = 'http://durandal.io/some/file.html';
+    const path = './other/module';
+
+    expect(relativeToFile(path, file)).toBe('http://durandal.io/some/other/module');
+  });
+
+  it('can make a double dot path relative to an absolute file', () => {
+    const file = 'http://durandal.io/some/file.html';
+    const path = '../other/module';
+
+    expect(relativeToFile(path, file)).toBe('http://durandal.io/other/module');
+  });
+
+  it('returns path if null file provided', () => {
+    const file = null;
+    const path = 'module';
+
+    expect(relativeToFile(path, file)).toBe('module');
+  });
+
+  it('returns path if empty file provided', () => {
+    const file = '';
+    const path = 'module';
+
+    expect(relativeToFile(path, file)).toBe('module');
+  });
+});
+
+describe('join', () => {
+  it('can combine two simple paths', () => {
+    const path1 = 'one';
+    const path2 = 'two';
+
+    expect(join(path1, path2)).toBe('one/two');
+  });
+
+  it('can combine an absolute path and a simple path', () => {
+    const path1 = '/one';
+    const path2 = 'two';
+
+    expect(join(path1, path2)).toBe('/one/two');
+  });
+
+  it('can combine an absolute path and a simple path with slash', () => {
+    const path1 = '/one';
+    const path2 = '/two';
+
+    expect(join(path1, path2)).toBe('/one/two');
+  });
+
+  it('can combine a single slash and a simple path', () => {
+    const path1 = '/';
+    const path2 = 'two';
+
+    expect(join(path1, path2)).toBe('/two');
+  });
+
+  it('can combine a single slash and a simple path with slash', () => {
+    const path1 = '/';
+    const path2 = '/two';
+
+    expect(join(path1, path2)).toBe('/two');
+  });
+
+  it('can combine an absolute path with protocol and a simple path', () => {
+    const path1 = 'http://durandal.io';
+    const path2 = 'two';
+
+    expect(join(path1, path2)).toBe('http://durandal.io/two');
+  });
+
+  it('can combine an absolute path with protocol and a simple path with slash', () => {
+    const path1 = 'http://durandal.io';
+    const path2 = '/two';
+
+    expect(join(path1, path2)).toBe('http://durandal.io/two');
+  });
+
+  it('can combine an absolute path and a simple path with a dot', () => {
+    const path1 = 'http://durandal.io';
+    const path2 = './two';
+
+    expect(join(path1, path2)).toBe('http://durandal.io/two');
+  });
+
+  it('can combine a simple path and a relative path', () => {
+    const path1 = 'one';
+    const path2 = '../two';
+
+    expect(join(path1, path2)).toBe('two');
+  });
+
+  it('can combine an absolute path and a relative path', () => {
+    const path1 = 'http://durandal.io/somewhere';
+    const path2 = '../two';
+
+    expect(join(path1, path2)).toBe('http://durandal.io/two');
+  });
+
+  it('can combine a protocol independent path and a simple path', () => {
+    const path1 = '//durandal.io';
+    const path2 = 'two';
+
+    expect(join(path1, path2)).toBe('//durandal.io/two');
+  });
+
+  it('can combine a protocol independent path and a simple path with slash', () => {
+    const path1 = '//durandal.io';
+    const path2 = '/two';
+
+    expect(join(path1, path2)).toBe('//durandal.io/two');
+  });
+
+  it('can combine a protocol independent path and a simple path with a dot', () => {
+    const path1 = '//durandal.io';
+    const path2 = './two';
+
+    expect(join(path1, path2)).toBe('//durandal.io/two');
+  });
+
+  it('can combine a protocol independent path and a relative path', () => {
+    const path1 = '//durandal.io/somewhere';
+    const path2 = '../two';
+
+    expect(join(path1, path2)).toBe('//durandal.io/two');
+  });
+
+  it('can combine a complex path and a relative path', () => {
+    const path1 = 'one/three';
+    const path2 = '../two';
+
+    expect(join(path1, path2)).toBe('one/two');
+  });
+
+  it('returns path2 if path1 null', () => {
+    const path1 = null;
+    const path2 = 'two';
+
+    expect(join(path1, path2)).toBe('two');
+  });
+
+  it('returns path2 if path1 empty', () => {
+    const path1 = '';
+    const path2 = 'two';
+
+    expect(join(path1, path2)).toBe('two');
+  });
+
+  it('returns path1 if path2 null', () => {
+    const path1 = 'one';
+    const path2 = null;
+
+    expect(join(path1, path2)).toBe('one');
+  });
+
+  it('returns path1 if path2 empty', () => {
+    const path1 = 'one';
+    const path2 = '';
+
+    expect(join(path1, path2)).toBe('one');
+  });
+
+  it('should retain leading .. in path1', () => {
+    const path1 = '../one';
+    const path2 = './two';
+
+    expect(join(path1, path2)).toBe('../one/two');
+  });
+
+  it('should retain consecutive leading .. in path1', () => {
+    const path1 = '../../one';
+    const path2 = './two';
+
+    expect(join(path1, path2)).toBe('../../one/two');
+  });
+
+  it('should handle .. in path1 and path2', () => {
+    const path1 = '../../one';
+    const path2 = '../two';
+
+    expect(join(path1, path2)).toBe('../../two');
+  });
+
+  it('should merge .. in path1 and path2', () => {
+    const path1 = '../../one';
+    const path2 = '../../two';
+
+    expect(join(path1, path2)).toBe('../../../two');
+  });
+
+  it('should retain consecutive leading .. but not other .. in path1', () => {
+    const path1 = '../../one/../three';
+    const path2 = './two';
+
+    expect(join(path1, path2)).toBe('../../three/two');
+  });
+
+  it('should respect a trailing slash', () => {
+    const path1 = 'one/';
+    const path2 = 'two/';
+
+    expect(join(path1, path2)).toBe('one/two/');
+  });
+
+  it('should respect file:/// protocol with three slashes (empty host)', () => {
+    const path1 = 'file:///one';
+    const path2 = '/two';
+
+    expect(join(path1, path2)).toBe('file:///one/two');
+  });
+
+  it('should respect file:// protocol with two slashes (host given)', () => {
+    const path1 = 'file://localhost:8080';
+    const path2 = '/two';
+
+    expect(join(path1, path2)).toBe('file://localhost:8080/two');
+  });
+
+  it('should allow scheme-relative URL that uses colons in the path', () => {
+    const path1 = '//localhost/one:/';
+    const path2 = '/two';
+
+    expect(join(path1, path2)).toBe('//localhost/one:/two');
+  });
+
+  it('should not add more than two leading slashes to http:// protocol', () => {
+    const path1 = 'http:///';
+    const path2 = '/two';
+
+    expect(join(path1, path2)).toBe('http://two');
+  });
+});
+
+describe('query strings', () => {
+  interface IBuildTestCase {
+    input: any;
+    traditional?: boolean;
+    output: string;
+  }
+  const testCases: IBuildTestCase[] = [
+    { input: undefined, output: '' },
+    { input: null, output: '' },
+    { input: {}, output: '' },
+    { input: { a: null }, output: '' },
+    { input: { '': 'a' }, output: '=a' },
+    { input: { a: 'b' }, output: 'a=b' },
+    { input: { a: 'b', c: 'd' }, output: 'a=b&c=d' },
+    { input: { a: 'b', c: 'd' }, traditional: true, output: 'a=b&c=d' },
+    { input: { a: 'b', c: null }, output: 'a=b' },
+    { input: { a: 'b', c: null }, traditional: true, output: 'a=b' },
+    { input: { a: ['b', 'c'] }, output: 'a%5B%5D=b&a%5B%5D=c' },
+    { input: { a: ['b', 'c'] }, traditional: true, output: 'a=b&a=c' },
+    { input: { '&': ['b', 'c'] }, output: '%26%5B%5D=b&%26%5B%5D=c' },
+    { input: { '&': ['b', 'c'] }, traditional: true, output: '%26=b&%26=c' },
+
+    { input: { a: '&' }, output: 'a=%26' },
+    { input: { '&': 'a' }, output: '%26=a' },
+    { input: { a: true }, output: 'a=true' },
+    { input: { '$test': true }, output: '$test=true' },
+
+    { input: { obj: { a: 5, b: "str", c: false } }, output: 'obj%5Ba%5D=5&obj%5Bb%5D=str&obj%5Bc%5D=false' },
+    { input: { obj: { a: 5, b: "str", c: false } }, traditional: true, output: 'obj=%5Bobject%20Object%5D' },
+    { input: { obj: { a: 5, b: undefined } }, output: 'obj%5Ba%5D=5' },
+
+    { input: { a: { b: ['c', 'd', ['f', 'g']] } }, output: 'a%5Bb%5D%5B%5D=c&a%5Bb%5D%5B%5D=d&a%5Bb%5D%5B2%5D%5B%5D=f&a%5Bb%5D%5B2%5D%5B%5D=g' },
+    { input: { a: { b: ['c', 'd', ['f', 'g']] } }, traditional: true, output: 'a=%5Bobject%20Object%5D' },
+    { input: { a: ['c', 'd', ['f', 'g']] }, traditional: true, output: 'a=c&a=d&a=f%2Cg' },
+    { input: { a: ['c', 'd', { f: 'g' }] }, traditional: true, output: 'a=c&a=d&a=%5Bobject%20Object%5D' },
+  ];
+
+  testCases.forEach(({ input, output, traditional }) => {
+    it(`builds ${input instanceof Object ? JSON.stringify(input) : input} to "${output}"`, () => {
+      expect(buildQueryString(input, traditional)).toBe(output);
+    });
+  });
+
+  interface IParseTestCase {
+    input: string;
+    output: any;
+  }
+
+  const parseTestCases: IParseTestCase[] = [
+    { input: '', output: {} },
+    { input: '=', output: {} },
+    { input: '&', output: {} },
+    { input: '?', output: {} },
+
+    { input: 'a', output: { a: true } },
+    { input: 'a&b', output: { a: true, b: true } },
+    { input: 'a=', output: { a: '' } },
+    { input: 'a=&b=', output: { a: '', b: '' } },
+
+    { input: 'a=b', output: { a: 'b' } },
+    { input: 'a=b&c=d', output: { a: 'b', c: 'd' } },
+    { input: 'a=b&&c=d', output: { a: 'b', c: 'd' } },
+    { input: 'a=b&a=c', output: { a: ['b', 'c'] } },
+
+    { input: 'a=b&c=d=', output: { a: 'b', c: 'd' } },
+    { input: 'a=b&c=d==', output: { a: 'b', c: 'd' } },
+
+    { input: 'a=%26', output: { a: '&' } },
+    { input: '%26=a', output: { '&': 'a' } },
+    { input: '%26[]=b&%26[]=c', output: { '&': ['b', 'c'] } },
+
+    { input: 'a[b]=c&a[d]=e', output: { a: { b: 'c', d: 'e' } } },
+    { input: 'a[b][c][d]=e', output: { a: { b: { c: { d: 'e' } } } } },
+    { input: 'a[b][]=c&a[b][]=d&a[b][2][]=f&a[b][2][]=g', output: { a: { b: ['c', 'd', ['f', 'g']] } } },
+    { input: 'a[0]=b', output: { a: ['b'] } },
+  ];
+
+  parseTestCases.forEach(({ input, output }) => {
+    it(`parses ${input} to "${JSON.stringify(output)}"`, () => {
+      expect(parseQueryString(input)).toEqual(output);
+    });
+  });
+
+  it('does not pollute prototype', () => {
+    const path1 = '__proto__[asdf]=asdf';
+    assert.throws(() => parseQueryString(path1), 'Prototype pollution detected');
+  });
+});

programming_language: TypeScript