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, '<')
115
+ .replace(/>/g, '>')
116
+ .replace(/"/g, '"')
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>