CVE-2021-41097: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: 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`.
CWE 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.
CVSS v3 base score: 7.5
Published: 2021-09-27T18:15Z
Repository: https://github.com/aurelia/path
Language: TypeScript
Files changed: src/index.ts, test/path.spec.ts
Commit date: 2021-06-09 16:30:20 +1000
Commit message: fix: security issue gh closes #44

Diff: diff --git a/src/index.js b/src/index.ts
similarity index 95%
rename from src/index.js
rename to src/index.ts
index ca209c5..8282def 100644
--- a/src/index.js
+++ b/src/index.ts
@@ -156,11 +156,11 @@ function buildParam(key: string, value: any, traditional?: boolean): Array<strin
 *
 * @param params Object containing the keys and values to be used.
 * @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++) {
     let key = keys[i];
     pairs = pairs.concat(buildParam(key, params[key], traditional));
@@ -201,20 +201,23 @@ function processScalarParam(existedParam: Object, value: Object): Object {
 *
 * @param queryParams root-level result 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;
     }
   }
 }
diff --git a/test/path.spec.js b/test/path.spec.js
deleted file mode 100644
index d8066a3..0000000
--- a/test/path.spec.js
+++ /dev/null
@@ -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']});
-  });
-});
diff --git a/test/path.spec.ts b/test/path.spec.ts
new file mode 100644
index 0000000..f4ce35d
--- /dev/null
+++ b/test/path.spec.ts
@@ -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');
+  });
+});

Vulnerable code: export function buildQueryString(params: Object, traditional?: Boolean): string {
function parseComplexParam(queryParams: Object, keys: Object, value: any): void {
    let key = keys[j] === '' ? currentParams.length : keys[j];
      currentParams = currentParams[key] = prevValue || (isNaN(keys[j + 1]) ? {} : []);
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']});
  });
});
Fixed code: export function buildQueryString(params?: Object, traditional?: boolean): string {
function parseComplexParam(queryParams: Object, keys: (string | number)[], value: any): void {
    let key = keys[j] === '' ? (currentParams as any).length : keys[j];
    if (key === '__proto__') {
      throw new Error('Prototype pollution detected.');
    }
      currentParams = currentParams[key] = prevValue || (isNaN(keys[j + 1] as number) ? {} : []);
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');
  });
});