Skip to content
This repository was archived by the owner on Sep 3, 2022. It is now read-only.

Fix Potential DOM-based XSS via prototype pollution #232

Merged
merged 4 commits into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 4.1.7 / 2021-03-17

- Fix Potential DOM-based XSS via prototype pollution

# 4.1.6 / 2020-11-24

- Update `trim` package to address ReDoS vulnerability
Expand Down
71 changes: 42 additions & 29 deletions lib/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import {
SegmentAnalytics,
SegmentOpts,
SegmentIntegration,
PageDefaults, Message
PageDefaults
} from './types';

import { pageDefaults } from './pageDefaults';

import cloneDeep from 'lodash.clonedeep'
import pick from 'lodash.pick'
import cloneDeep from 'lodash.clonedeep';
import pick from 'lodash.pick';

import url from 'component-url';

var _analytics = global.analytics;

Expand All @@ -37,7 +39,6 @@ var extend = require('extend');
var cookie = require('./cookie');
var metrics = require('./metrics');
var debug = require('debug');
var defaults = require('@ndhoule/defaults');
var group = require('./group');
var is = require('is');
var isMeta = require('@segment/is-meta');
Expand All @@ -46,7 +47,7 @@ var nextTick = require('next-tick');
var normalize = require('./normalize');
var on = require('component-event').bind;
var prevent = require('@segment/prevent-default');
var querystring = require('component-querystring');

var store = require('./store');
var user = require('./user');
var type = require('component-type');
Expand All @@ -69,7 +70,6 @@ function Analytics() {
this.log = debug('analytics.js');
bindAll(this);


const self = this;
this.on('initialize', function(_, options) {
if (options.initialPageview) self.page();
Expand Down Expand Up @@ -177,8 +177,8 @@ Analytics.prototype.init = Analytics.prototype.initialize = function(

// add integrations
Object.keys(settings).forEach(key => {
const opts = settings[key]
const name = key
const opts = settings[key];
const name = key;

// Don't load disabled integrations
if (options.integrations) {
Expand Down Expand Up @@ -225,7 +225,7 @@ Analytics.prototype.init = Analytics.prototype.initialize = function(
this.failedInitializations = [];
let initialPageSkipped = false;
Object.keys(integrations).forEach(key => {
const integration = integrations[key]
const integration = integrations[key];
if (
options.initialPageview &&
integration.options.initialPageview === false
Expand Down Expand Up @@ -331,7 +331,7 @@ Analytics.prototype.identify = function(
msg.integrations = {
...this.options.integrations,
...msg.integrations
}
};
}

this._invoke('identify', new Identify(msg));
Expand Down Expand Up @@ -392,7 +392,7 @@ Analytics.prototype.group = function(
msg.integrations = {
...this.options.integrations,
...msg.integrations
}
};
}

this._invoke('group', new Group(msg));
Expand Down Expand Up @@ -461,7 +461,7 @@ Analytics.prototype.track = function(
msg.integrations = {
...this._mergeInitializeAndPlanIntegrations(planIntegrationOptions),
...msg.integrations
}
};

this._invoke('track', new Track(msg));

Expand All @@ -487,22 +487,22 @@ Analytics.prototype.trackClick = Analytics.prototype.trackLink = function(
event: any,
properties?: any
): SegmentAnalytics {
let elements: Array<Element> = []
let elements: Array<Element> = [];
if (!links) return this;
// always arrays, handles jquery
if (links instanceof Element) {
elements = [links]
} else if ("toArray" in links) {
elements = links.toArray()
elements = [links];
} else if ('toArray' in links) {
elements = links.toArray();
} else {
elements = links as Array<Element>
elements = links as Array<Element>;
}

elements.forEach(el => {
if (type(el) !== 'element') {
throw new TypeError('Must pass HTMLElement to `analytics.trackLink`.');
}
on(el, 'click', (e) => {
on(el, 'click', e => {
const ev = is.fn(event) ? event(el) : event;
const props = is.fn(properties) ? properties(el) : properties;
const href =
Expand Down Expand Up @@ -546,12 +546,12 @@ Analytics.prototype.trackSubmit = Analytics.prototype.trackForm = function(
// always arrays, handles jquery
if (type(forms) === 'element') forms = [forms];

const elements = forms as Array<unknown>
const elements = forms as Array<unknown>;

elements.forEach((el: { submit: () => void }) => {
if (type(el) !== 'element')
throw new TypeError('Must pass HTMLElement to `analytics.trackForm`.');
const handler = (e) => {
const handler = e => {
prevent(e);

const ev = is.fn(event) ? event(el) : event;
Expand All @@ -561,7 +561,7 @@ Analytics.prototype.trackSubmit = Analytics.prototype.trackForm = function(
this._callback(function() {
el.submit();
});
}
};

// Support the events happening through jQuery or Zepto instead of through
// the normal DOM API, because `el.submit` doesn't bubble up events...
Expand Down Expand Up @@ -618,11 +618,11 @@ Analytics.prototype.page = function(
// This bug made it so we only would overwrite values in `defs` that were set to `undefined`.
// In some cases, though, pageDefaults will return defaults with values set to "" (such as `window.location.search` defaulting to "").
// The decision to not fix this bus was made to preserve backwards compatibility.
const defs = pageDefaults()
const defs = pageDefaults();
properties = {
...properties,
...defs
}
};

// Mirror user overrides to `options.context.page` (but exclude custom properties)
// (Any page defaults get applied in `this.normalize` for consistency.)
Expand All @@ -648,7 +648,7 @@ Analytics.prototype.page = function(
msg.integrations = {
...this.options.integrations,
...msg.integrations
}
};
}

this._invoke('page', new Page(msg));
Expand Down Expand Up @@ -706,7 +706,7 @@ Analytics.prototype.alias = function(
msg.integrations = {
...this.options.integrations,
...msg.integrations
}
};
}

this._invoke('alias', new Alias(msg));
Expand Down Expand Up @@ -840,8 +840,8 @@ Analytics.prototype._invoke = function(
function applyIntegrationMiddlewares(facade) {
let failedInitializations = self.failedInitializations || [];
Object.keys(self._integrations).forEach(key => {
const integration = self._integrations[key]
const { name } = integration
const integration = self._integrations[key];
const { name } = integration;
const facadeCopy = extend(true, new Facade({}), facade);

if (!facadeCopy.enabled(name)) return;
Expand Down Expand Up @@ -960,9 +960,22 @@ Analytics.prototype.reset = function() {
* @api private
*/

interface QueryStringParams {
[key: string]: string | null;
}

Analytics.prototype._parseQuery = function(query: string): SegmentAnalytics {
// Parse querystring to an object
var q = querystring.parse(query);
const parsed = url.parse(query);

const q = parsed.query
.split('&')
.reduce((acc: QueryStringParams, str: string) => {
const [k, v] = str.split('=');
acc[k] = decodeURI(v).replace('+', ' ');
return acc;
}, {});

// Create traits and properties objects, populate from querysting params
var traits = pickPrefix('ajs_trait_', q);
var props = pickPrefix('ajs_prop_', q);
Expand Down Expand Up @@ -998,7 +1011,7 @@ Analytics.prototype._parseQuery = function(query: string): SegmentAnalytics {
*/

Analytics.prototype.normalize = function(msg: {
options: { [key: string]: unknown }
options: { [key: string]: unknown };
context: { page: Partial<PageDefaults> };
anonymousId: string;
}): object {
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"bind-all": "^1.0.0",
"component-emitter": "^1.2.1",
"component-event": "^0.1.4",
"component-querystring": "^2.0.0",
"component-type": "^1.2.1",
"component-url": "^0.2.1",
"debug": "^2.6.9",
Expand Down
Loading