Skip to content

Commit cf87968

Browse files
authored
feat: Introduce 'sanitizeKeys' config option (#1264)
* feat: Introduce 'sanitizeKeys' config option * ref: Handle cyclic references and support RegExp in * docs: Make docs a tad better
1 parent 49a2fa2 commit cf87968

File tree

7 files changed

+245
-59
lines changed

7 files changed

+245
-59
lines changed

docs/config.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,21 @@ Those configuration options are documented below:
149149
sampleRate: 0.5 // send 50% of events, drop the other half
150150
}
151151
152+
.. describe:: sanitizeKeys
153+
154+
An array of strings or regex patterns representing keys that should be scrubbed from the payload sent to Sentry.
155+
We'll go through every field in the payload and mask the values with simple `********` string instead.
156+
This will match *only* keys of the object, not the values.
157+
Sentry also sanitize all events sent to it on the server-side, but this allows you to strip the payload before it gets to the server.
158+
159+
160+
.. code-block:: javascript
161+
162+
{
163+
sanitizeKeys: [/_token$/, /password/i, 'someHidiousKey']
164+
}
165+
166+
152167
.. describe:: dataCallback
153168

154169
A function that allows mutation of the data payload right before being

src/raven.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ var supportsFetch = utils.supportsFetch;
3232
var supportsReferrerPolicy = utils.supportsReferrerPolicy;
3333
var serializeKeysForMessage = utils.serializeKeysForMessage;
3434
var serializeException = utils.serializeException;
35+
var sanitize = utils.sanitize;
3536

3637
var wrapConsoleMethod = require('./console').wrapMethod;
3738

@@ -85,13 +86,13 @@ function Raven() {
8586
collectWindowErrors: true,
8687
captureUnhandledRejections: true,
8788
maxMessageLength: 0,
88-
8989
// By default, truncates URL values to 250 chars
9090
maxUrlLength: 250,
9191
stackTraceLimit: 50,
9292
autoBreadcrumbs: true,
9393
instrument: true,
94-
sampleRate: 1
94+
sampleRate: 1,
95+
sanitizeKeys: []
9596
};
9697
this._fetchDefaults = {
9798
method: 'POST',
@@ -1865,6 +1866,8 @@ Raven.prototype = {
18651866
// Include server_name if it's defined in globalOptions
18661867
if (globalOptions.serverName) data.server_name = globalOptions.serverName;
18671868

1869+
data = this._sanitizeData(data);
1870+
18681871
// Cleanup empty properties before sending them to the server
18691872
Object.keys(data).forEach(function(key) {
18701873
if (data[key] == null || data[key] === '' || isEmptyObject(data[key])) {
@@ -1905,6 +1908,10 @@ Raven.prototype = {
19051908
}
19061909
},
19071910

1911+
_sanitizeData: function(data) {
1912+
return sanitize(data, this._globalOptions.sanitizeKeys);
1913+
},
1914+
19081915
_getUuid: function() {
19091916
return uuid4();
19101917
},

src/utils.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,44 @@ function serializeKeysForMessage(keys, maxLength) {
536536
return '';
537537
}
538538

539+
function sanitize(input, sanitizeKeys) {
540+
if (!isArray(sanitizeKeys) || (isArray(sanitizeKeys) && sanitizeKeys.length === 0))
541+
return input;
542+
543+
var sanitizeRegExp = joinRegExp(sanitizeKeys);
544+
var sanitizeMask = '********';
545+
var safeInput;
546+
547+
try {
548+
safeInput = JSON.parse(stringify(input));
549+
} catch (o_O) {
550+
return input;
551+
}
552+
553+
function sanitizeWorker(workerInput) {
554+
if (isArray(workerInput)) {
555+
return workerInput.map(function(val) {
556+
return sanitizeWorker(val);
557+
});
558+
}
559+
560+
if (isPlainObject(workerInput)) {
561+
return Object.keys(workerInput).reduce(function(acc, k) {
562+
if (sanitizeRegExp.test(k)) {
563+
acc[k] = sanitizeMask;
564+
} else {
565+
acc[k] = sanitizeWorker(workerInput[k]);
566+
}
567+
return acc;
568+
}, {});
569+
}
570+
571+
return workerInput;
572+
}
573+
574+
return sanitizeWorker(safeInput);
575+
}
576+
539577
module.exports = {
540578
isObject: isObject,
541579
isError: isError,
@@ -567,5 +605,6 @@ module.exports = {
567605
fill: fill,
568606
safeJoin: safeJoin,
569607
serializeException: serializeException,
570-
serializeKeysForMessage: serializeKeysForMessage
608+
serializeKeysForMessage: serializeKeysForMessage,
609+
sanitize: sanitize
571610
};

test/raven.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,33 @@ describe('globals', function() {
576576
assert.isTrue(Raven._send.calledOnce);
577577
});
578578

579+
it('should respect `sanitizeKeys`', function() {
580+
this.sinon.stub(Raven, '_sendProcessedPayload');
581+
Raven._globalOptions.sanitizeKeys = ['password', 'token'];
582+
Raven.captureMessage('hello', {
583+
extra: {
584+
password: 'foo',
585+
token: 'abc',
586+
user: 'rick'
587+
},
588+
user: {
589+
password: 'foo'
590+
}
591+
});
592+
593+
// It's not the main thing we test here and it's a variable with every run
594+
delete Raven._sendProcessedPayload.lastCall.args[0].extra['session:duration'];
595+
596+
assert.deepEqual(Raven._sendProcessedPayload.lastCall.args[0].extra, {
597+
password: '********',
598+
token: '********',
599+
user: 'rick'
600+
});
601+
assert.deepEqual(Raven._sendProcessedPayload.lastCall.args[0].user, {
602+
password: '********'
603+
});
604+
});
605+
579606
it('should send a proper payload with frames', function() {
580607
this.sinon.stub(Raven, '_send');
581608

test/utils.test.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var parseUrl = utils.parseUrl;
2626
var safeJoin = utils.safeJoin;
2727
var serializeException = utils.serializeException;
2828
var serializeKeysForMessage = utils.serializeKeysForMessage;
29+
var sanitize = utils.sanitize;
2930

3031
describe('utils', function() {
3132
describe('isUndefined', function() {
@@ -696,4 +697,105 @@ describe('utils', function() {
696697
assert.equal(serializeKeysForMessage('foo'), 'foo');
697698
});
698699
});
700+
701+
describe('sanitize', function() {
702+
var sanitizeMask = '********';
703+
704+
it('should return simple values directly', function() {
705+
var actual = sanitize('foo');
706+
var expected = 'foo';
707+
assert.deepEqual(actual, expected);
708+
});
709+
710+
it('should return same value when no sanitizeKeys passed', function() {
711+
var actual = sanitize({foo: 42});
712+
var expected = {foo: 42};
713+
assert.deepEqual(actual, expected);
714+
});
715+
716+
it('should return same value when empty sanitizeKeys array passed', function() {
717+
var actual = sanitize({foo: 42}, []);
718+
var expected = {foo: 42};
719+
assert.deepEqual(actual, expected);
720+
});
721+
722+
it('should sanitize flat objects', function() {
723+
var actual = sanitize({foo: 42}, ['foo']);
724+
var expected = {foo: sanitizeMask};
725+
assert.deepEqual(actual, expected);
726+
});
727+
728+
it('should sanitize flat objects with multiple keys', function() {
729+
var actual = sanitize({foo: 42, bar: 'abc', baz: 1337}, ['foo', 'baz']);
730+
var expected = {foo: sanitizeMask, bar: 'abc', baz: sanitizeMask};
731+
assert.deepEqual(actual, expected);
732+
});
733+
734+
it('should sanitize flat objects when value is a plain object or array', function() {
735+
var actual = sanitize({foo: {bar: 42}}, ['foo']);
736+
var expected = {foo: sanitizeMask};
737+
assert.deepEqual(actual, expected);
738+
739+
actual = sanitize({foo: [42, 'abc']}, ['foo']);
740+
expected = {foo: sanitizeMask};
741+
assert.deepEqual(actual, expected);
742+
});
743+
744+
it('should sanitize nested objects keys', function() {
745+
var actual = sanitize({foo: {bar: 42}}, ['bar']);
746+
var expected = {foo: {bar: sanitizeMask}};
747+
assert.deepEqual(actual, expected);
748+
});
749+
750+
it('should sanitize objects nested in arrays', function() {
751+
var actual = sanitize({foo: [{bar: 42}, 42]}, ['bar']);
752+
var expected = {foo: [{bar: sanitizeMask}, 42]};
753+
assert.deepEqual(actual, expected);
754+
});
755+
756+
it('should sanitize every object when array provided as input', function() {
757+
var actual = sanitize([{foo: 42}, {bar: 42}, 42], ['foo', 'bar']);
758+
var expected = [{foo: sanitizeMask}, {bar: sanitizeMask}, 42];
759+
assert.deepEqual(actual, expected);
760+
});
761+
762+
it('shouldnt break with cyclic references', function() {
763+
var input = {
764+
foo: {},
765+
baz: 42
766+
};
767+
input.foo.bar = input.foo;
768+
769+
var actual = sanitize(input, ['baz']);
770+
var expected = {foo: {bar: '[Circular ~.foo]'}, baz: sanitizeMask};
771+
assert.deepEqual(actual, expected);
772+
});
773+
774+
it('should work with keys as RegExps', function() {
775+
var actual = sanitize(
776+
{
777+
foo: {
778+
bar: 42,
779+
baz: 1337,
780+
qux: 'rick',
781+
forgotFifthWord: 'whoops',
782+
thisShouldMatch123_32: 'hello',
783+
butThisNot123_42_X: 'morty'
784+
}
785+
},
786+
[/^ba/i, 'forgotFifthWord', /\d{3}_\d{2}$/i]
787+
);
788+
var expected = {
789+
foo: {
790+
bar: sanitizeMask,
791+
baz: sanitizeMask,
792+
qux: 'rick',
793+
forgotFifthWord: sanitizeMask,
794+
thisShouldMatch123_32: sanitizeMask,
795+
butThisNot123_42_X: 'morty'
796+
}
797+
};
798+
assert.deepEqual(actual, expected);
799+
});
800+
});
699801
});

0 commit comments

Comments
 (0)