@cryptotaxi247 / netdata-1 / commits / 1d81008b9

global XSS protection for netdata

Costa Tsaousis (ktsaou) committed Jan 28, 2018 at 23:35 UTC 1d81008b9707e078b298a1594ce7a7806c2f4df1
2 files changed +115 -4
web/dashboard.js
+107
@@ -61,6 +61,8 @@
61 * (default: undefined) */
62 /*global netdataIntersectionObserver *//* boolean, enable or disable the use of intersection observer
63 * (default: true) */
64 +/*global netdataCheckXSS *//* boolean, enable or disable checking for XSS issues
65 + * (default: false) */
66
67 // ----------------------------------------------------------------------------
68 // global namespace
@@ -99,6 +101,94 @@ var NETDATA = window.NETDATA || {};
101 .replace(/\//g, '_');
102 };
103
104 + // ----------------------------------------------------------------------------------------------------------------
105 + // XSS checks
106 +
107 + NETDATA.xss = {
108 + enabled: (typeof netdataCheckXSS === 'undefined')?false:netdataCheckXSS,
109 +
110 + string: function (s) {
111 + if (typeof s === 'string' || typeof s === 'number' || typeof s === 'boolean')
112 + return s.toString()
113 + .replace(/&/g, '&')
114 + .replace(/</g, '&lt;')
115 + .replace(/>/g, '&gt;')
116 + .replace(/"/g, '&quot;')
117 + .replace(/'/g, '#27;');
118 +
119 + return '';
120 + },
121 +
122 + object: function(name, obj, ignore_regex) {
123 + if(obj === null) return obj;
124 +
125 + var type = typeof(obj);
126 + if(type === 'undefined' || type === 'number') return obj;
127 +
128 + if(typeof ignore_regex !== 'undefined') {
129 + if(ignore_regex.test(name) === true) {
130 + // console.log('XSS: ignoring "' + name + '"');
131 + return obj;
132 + }
133 + }
134 +
135 + if(type === 'object' && Array.isArray(obj))
136 + type = 'array';
137 +
138 + var ret, i, len;
139 + switch (type) {
140 + case 'string':
141 + ret = this.string(obj);
142 + if(ret !== obj) console.log('XSS protection changed string ' + name + ' from "' + obj + '" to "' + ret + '"');
143 + return ret;
144 +
145 + case 'object':
146 + for(i in obj) {
147 + if(obj.hasOwnProperty(i) === false) continue;
148 + if(this.string(i) !== i) {
149 + console.log('XSS protection removed invalid object member "' + name + '.' + i + '"');
150 + delete obj[i];
151 + }
152 + else
153 + obj[i] = this.object(name + '.' + i, obj[i], ignore_regex);
154 + }
155 + return obj;
156 +
157 + case 'array':
158 + len = obj.length;
159 + while(len--)
160 + obj[len] = this.object(name + '[' + len + ']', obj[len], ignore_regex);
161 +
162 + return obj;
163 +
164 + default:
165 + return obj;
166 + }
167 + },
168 +
169 + checkOptional: function(name, obj, ignore_pattern) {
170 + if(this.enabled === true) {
171 + var regex;
172 + if(typeof ignore_pattern !== 'undefined')
173 + regex = new RegExp(ignore_pattern);
174 +
175 + //console.log('XSS: checking "' + name + '"...');
176 + return this.object(name, obj, regex);
177 + }
178 + return obj;
179 + },
180 +
181 + checkAlways: function(name, obj, ignore_pattern) {
182 + //console.log('XSS: checking "' + name + '"...');
183 +
184 + var regex;
185 + if(typeof ignore_pattern === 'string')
186 + regex = new RegExp(ignore_pattern);
187 +
188 + return this.object(name, obj, regex);
189 + }
190 + };
191 +
192 // ----------------------------------------------------------------------------------------------------------------
193 // Detect the netdata server
194
@@ -1469,6 +1559,7 @@ var NETDATA = window.NETDATA || {};
1559 xhrFields: {withCredentials: true} // required for the cookie
1560 })
1561 .done(function (data) {
1562 + data = NETDATA.xss.checkOptional('/api/v1/charts', data);
1563 got_data(host, data, callback);
1564 })
1565 .fail(function () {
@@ -5191,6 +5282,8 @@ var NETDATA = window.NETDATA || {};
5282 xhrFields: { withCredentials: true } // required for the cookie
5283 })
5284 .done(function(chart) {
5285 + chart = NETDATA.xss.checkOptional('/api/v1/chart', chart);
5286 +
5287 chart.url = that.chart_url;
5288 that.__defaultsFromDownloadedChart(chart);
5289 NETDATA.chartRegistry.add(that.host, that.id, chart);
@@ -8919,6 +9012,8 @@ var NETDATA = window.NETDATA || {};
9012 xhrFields: { withCredentials: true } // required for the cookie
9013 })
9014 .done(function(data) {
9015 + data = NETDATA.xss.checkOptional('/api/v1/alarms', data, '.*\.(calc|calc_parsed|warn|warn_parsed|crit|crit_parsed)$');
9016 +
9017 if(NETDATA.alarms.first_notification_id === 0 && typeof data.latest_alarm_log_unique_id === 'number')
9018 NETDATA.alarms.first_notification_id = data.latest_alarm_log_unique_id;
9019
@@ -8970,6 +9065,8 @@ var NETDATA = window.NETDATA || {};
9065 xhrFields: { withCredentials: true } // required for the cookie
9066 })
9067 .done(function(data) {
9068 + data = NETDATA.xss.checkOptional('/api/v1/alarm_log', data);
9069 +
9070 if(typeof callback === 'function')
9071 return callback(data);
9072 })
@@ -9106,6 +9203,8 @@ var NETDATA = window.NETDATA || {};
9203 data = null;
9204 }
9205
9206 + data = NETDATA.xss.checkOptional('/api/v1/registry?action=hello', data);
9207 +
9208 if(typeof callback === 'function')
9209 return callback(data);
9210 })
@@ -9134,6 +9233,8 @@ var NETDATA = window.NETDATA || {};
9233 xhrFields: { withCredentials: true } // required for the cookie
9234 })
9235 .done(function(data) {
9236 + data = NETDATA.xss.checkAlways('/api/v1/registry?action=access', data);
9237 +
9238 var redirect = null;
9239 if(typeof data.registry === 'string')
9240 redirect = data.registry;
@@ -9182,6 +9283,8 @@ var NETDATA = window.NETDATA || {};
9283 xhrFields: { withCredentials: true } // required for the cookie
9284 })
9285 .done(function(data) {
9286 + data = NETDATA.xss.checkAlways('/api/v1/registry?action=delete', data);
9287 +
9288 if(typeof data.status !== 'string' || data.status !== 'ok') {
9289 NETDATA.error(411, NETDATA.registry.server + ' responded with: ' + JSON.stringify(data));
9290 data = null;
@@ -9211,6 +9314,8 @@ var NETDATA = window.NETDATA || {};
9314 xhrFields: { withCredentials: true } // required for the cookie
9315 })
9316 .done(function(data) {
9317 + data = NETDATA.xss.checkAlways('/api/v1/registry?action=search', data);
9318 +
9319 if(typeof data.status !== 'string' || data.status !== 'ok') {
9320 NETDATA.error(417, NETDATA.registry.server + ' responded with: ' + JSON.stringify(data));
9321 data = null;
@@ -9240,6 +9345,8 @@ var NETDATA = window.NETDATA || {};
9345 xhrFields: { withCredentials: true } // required for the cookie
9346 })
9347 .done(function(data) {
9348 + data = NETDATA.xss.checkAlways('/api/v1/registry?action=switch', data);
9349 +
9350 if(typeof data.status !== 'string' || data.status !== 'ok') {
9351 NETDATA.error(413, NETDATA.registry.server + ' responded with: ' + JSON.stringify(data));
9352 data = null;
web/index.html
+8 -4
@@ -674,6 +674,7 @@
674 if(urlOptions.server !== null && urlOptions.server !== '') {
675 netdataServerStatic = document.location.origin.toString() + document.location.pathname.toString();
676 netdataServer = urlOptions.server;
677 + netdataCheckXSS = true;
678 }
679 else
680 urlOptions.server = null;
@@ -3278,12 +3279,14 @@
3279
3280
3281 function loadSnapshotPreflightFile(file) {
3282 + var filename = NETDATA.xss.string(file.name);
3283 var fr = new FileReader();
3284 fr.onload = function(e) {
3283 - document.getElementById('loadSnapshotFilename').innerHTML = file.name;
3285 + document.getElementById('loadSnapshotFilename').innerHTML = filename;
3286 var result = null;
3287 try {
3286 - result = JSON.parse(e.target.result);
3288 + result = NETDATA.xss.checkAlways('snapshot', JSON.parse(e.target.result), '^(snapshot\.info|snapshot\.data)$');
3289 +
3290 //console.log(result);
3291 var date_after = new Date(result.after_ms);
3292 var date_before = new Date(result.before_ms);
@@ -3300,7 +3303,7 @@
3303 if (typeof result.data_size === 'undefined')
3304 result.data_size = 0;
3305
3303 - document.getElementById('loadSnapshotFilename').innerHTML = '<code>' + file.name + '</code>';
3306 + document.getElementById('loadSnapshotFilename').innerHTML = '<code>' + filename + '</code>';
3307 document.getElementById('loadSnapshotHostname').innerHTML = '<b>' + result.hostname + '</b>, netdata version: <b>' + result.netdata_version.toString() + '</b>';
3308 document.getElementById('loadSnapshotURL').innerHTML = result.url;
3309 document.getElementById('loadSnapshotCharts').innerHTML = result.charts.charts_count.toString() + ' charts, ' + result.charts.dimensions_count.toString() + ' dimensions, ' + result.data_points.toString() + ' points per dimension, ' + Math.round(result.duration_ms / result.data_points).toString() + ' ms per point';
@@ -4666,6 +4669,7 @@
4669 </p>
4670 </div>
4671 <div class="modal-footer">
4672 + <span style="display: inline-block; padding-right: 20px;">Snapshot files contain both data and javascript code. Make sure <b>you trust the files</b> you import!</span>
4673 <a id="loadSnapshotImport" href="#" onclick="loadSnapshot(); return false;" type="button" class="btn btn-success disabled">Import</a>
4674 <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
4675 </div>
@@ -5625,6 +5629,6 @@
5629 </div>
5630 </div>
5631 <div id="hiddenDownloadLinks" style="display: none;" hidden></div>
5628 - <script type="text/javascript" src="dashboard.js?v20180127-1"></script>
5632 + <script type="text/javascript" src="dashboard.js?v20180128-2"></script>
5633 </body>
5634 </html>