signal-desktop/test/app/logging_test.js

275 lines
8.8 KiB
JavaScript
Raw Normal View History

Security: Replace Unicode order overrides in attachment names As a user, when I receive a file attachment, I want to have confidence that the filename I see in the Signal Desktop app is the same as it will be on disk. To prevent user confusion when receiving files with Unicode order override characters, e.g. `test<LTRO>fig.exe` appearing as `testexe.gif`, we replace all occurrences of order overrides (`U+202D` and `U+202E`) with `U+FFFD`. **Changes** - [x] Bump `Attachment` `schemaVersion` to 2. - [x] Replace all Unicode order overrides in `attachment.filename`: `Attachment.replaceUnicodeOrderOverrides`. - [x] Add tests for existing `Attachment.upgradeSchema` - [x] Add tests for existing `Attachment.withSchemaVersion` - [x] Add tests for `Attachment.replaceUnicodeOrderOverrides` positives. - [x] Add `testcheck` generative property-based testing library (based on QuickCheck) to ensure valid filenames are preserved. --- commit 855bdbc7e647e44f73b9e1f5e6d64f734c61169a Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 13:02:01 2018 -0500 Log error stack in case of error commit 6e053ed66aee136f186568fa88aacd4814b2ab07 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:30:28 2018 -0500 Improve `upgradeStep` error handling commit 8c226a2523b701cb578b2137832c3eaf3475bb2b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:30:08 2018 -0500 Check for expected version before upgrade Prevents out of order upgrade steps. commit 28b0675591e782169128f75429b7bab2a22307fa Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:29:52 2018 -0500 Reject invalid attachments commit 41f4f457dae9416dae66dc2fa2079483d1f127a9 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:29:36 2018 -0500 Fix upgrade pipeline order commit 3935629e91c49b8d96c1e02bd37b1b31d1180720 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:28:25 2018 -0500 Avoid `_.isPlainObject` Attachments are deserialized from a protocol buffer and can have a non-plain-object constructor. commit 39f6e7f622ff4885e2ccafa354e0edb5864c55d8 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:19:07 2018 -0500 Define basic attachment validity commit adcf7e3243cd90866cc35990c558ff7829019037 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:18:54 2018 -0500 Add tests for attachment upgrade pipeline commit 82fc4644d7e654eea9f348518b086497be2b0cb4 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 21 12:20:24 2018 -0500 Favor `async` / `await` over `then` commit 8fe49e3c40e78ced0b8f2eb0b678f4bae842855d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 21 12:19:59 2018 -0500 Add `eslint-more` plugin This will enable us to disallow `then` in favor of `async` / `await`. commit 020beefb25f508ae96cf3fc099599fbbca98802b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 21 11:31:49 2018 -0500 Remove unnecessary `async` modifiers commit 177090c5f5ad9836f0ca0a5c2f298779519e3692 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 21 11:30:55 2018 -0500 Document `operator-linebreak` ESLint rule commit 25622b7c59291cb672ae057c47e7327a564cca40 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 21 11:14:15 2018 -0500 Prefix internal function with `_` commit 6aa3cf5098df71e9b710064739ec49d74f81b7bf Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 19:00:07 2018 -0500 Replace all Unicode order override occurrences commit fd6e23b0a519bce3c12c5b9ac676bcd198034fed Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:48:41 2018 -0500 Whitelist `testcheck` `check` and `gen` globals commit 400bae9fac5078821813bc0ca17a5d7a72900161 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:46:57 2018 -0500 :art: Fix lint errors commit da53d3960aa7aa36b7cc1fcff414c9e929c0d9fc Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:42:42 2018 -0500 Add tests for `Attachment.withSchemaVersion` commit ec203444239d9e3c443ba88cab7ef4672151072d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:42:17 2018 -0500 Add test for `Attachment.upgradeSchema` commit 4540d5bdf7a4279f49d2e4c6ee03f47b93df46bf Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:05:29 2018 -0500 Rename `setSchemaVersion` --> `withSchemaVersion` Put the schema version first for better readability. commit e379cf919feda31d1fa96d406c30fd38e159a11d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:03:22 2018 -0500 Add filename sanitization to upgrade pipeline commit 1e344a0d15926fc3e17be20cd90bfa882b65f337 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:01:55 2018 -0500 Test that we preserve non-suspicious filenames commit a2452bfc98f93f82bed48b438757af2e66a6af82 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:00:56 2018 -0500 Add `testcheck` dependency Allows for generative property-based testing similar to Haskell’s QuickCheck. See: https://medium.com/javascript-inside/f91432247c27 commit ceb5bfd2484a77689fdb8e9edd18d4a7b093a486 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 16:15:33 2018 -0500 Replace Unicode order override characters Prevents users from being tricked into clicking a file named `testexe.fig` that appears as `testexe.gif` due to a Unicode order override character. See: - http://unicode.org/reports/tr36/#Bidirectional_Text_Spoofing - https://krebsonsecurity.com/2011/09/right-to-left-override-aids-email-attacks/ commit bc605afb1c6af3a5ebc31a4c1523ff170eb96ffe Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 16:12:29 2018 -0500 Remove `CURRENT_PROCESS_VERSION` Reintroduce this whenever we need it. We currently only deal with schema version numbers within this module.
2018-02-22 18:21:53 +00:00
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
/* eslint-disable more/no-then */
const fs = require('fs');
const path = require('path');
const tmp = require('tmp');
const { expect } = require('chai');
const {
eliminateOutOfDateFiles,
eliminateOldEntries,
isLineAfterDate,
fetchLog,
fetch,
} = require('../../app/logging');
describe('app/logging', () => {
let basePath;
let tmpDir;
beforeEach(() => {
tmpDir = tmp.dirSync({
unsafeCleanup: true,
});
basePath = tmpDir.name;
});
afterEach((done) => {
// we need the unsafe option to recursively remove the directory
tmpDir.removeCallback(done);
});
describe('#isLineAfterDate', () => {
it('returns false if falsy', () => {
const actual = isLineAfterDate('', new Date());
expect(actual).to.equal(false);
});
it('returns false if invalid JSON', () => {
const actual = isLineAfterDate('{{}', new Date());
expect(actual).to.equal(false);
});
it('returns false if date is invalid', () => {
const line = JSON.stringify({ time: '2018-01-04T19:17:05.014Z' });
const actual = isLineAfterDate(line, new Date('try6'));
expect(actual).to.equal(false);
});
it('returns false if log time is invalid', () => {
const line = JSON.stringify({ time: 'try7' });
const date = new Date('2018-01-04T19:17:00.000Z');
const actual = isLineAfterDate(line, date);
expect(actual).to.equal(false);
});
it('returns false if date before provided date', () => {
const line = JSON.stringify({ time: '2018-01-04T19:17:00.000Z' });
const date = new Date('2018-01-04T19:17:05.014Z');
const actual = isLineAfterDate(line, date);
expect(actual).to.equal(false);
});
it('returns true if date is after provided date', () => {
const line = JSON.stringify({ time: '2018-01-04T19:17:05.014Z' });
const date = new Date('2018-01-04T19:17:00.000Z');
const actual = isLineAfterDate(line, date);
expect(actual).to.equal(true);
});
});
describe('#eliminateOutOfDateFiles', () => {
it('deletes an empty file', () => {
const date = new Date();
const log = '\n';
const target = path.join(basePath, 'log.log');
fs.writeFileSync(target, log);
return eliminateOutOfDateFiles(basePath, date).then(() => {
expect(fs.existsSync(target)).to.equal(false);
});
});
it('deletes a file with invalid JSON lines', () => {
const date = new Date();
const log = '{{}\n';
const target = path.join(basePath, 'log.log');
fs.writeFileSync(target, log);
return eliminateOutOfDateFiles(basePath, date).then(() => {
expect(fs.existsSync(target)).to.equal(false);
});
});
it('deletes a file with all dates before provided date', () => {
const date = new Date('2018-01-04T19:17:05.014Z');
const contents = [
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const target = path.join(basePath, 'log.log');
fs.writeFileSync(target, contents);
return eliminateOutOfDateFiles(basePath, date).then(() => {
expect(fs.existsSync(target)).to.equal(false);
});
});
it('keeps a file with first line date before provided date', () => {
const date = new Date('2018-01-04T19:16:00.000Z');
const contents = [
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const target = path.join(basePath, 'log.log');
fs.writeFileSync(target, contents);
return eliminateOutOfDateFiles(basePath, date).then(() => {
expect(fs.existsSync(target)).to.equal(true);
});
});
it('keeps a file with last line date before provided date', () => {
const date = new Date('2018-01-04T19:17:01.000Z');
const contents = [
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const target = path.join(basePath, 'log.log');
fs.writeFileSync(target, contents);
return eliminateOutOfDateFiles(basePath, date).then(() => {
expect(fs.existsSync(target)).to.equal(true);
});
});
});
describe('#eliminateOldEntries', () => {
it('eliminates all non-parsing entries', () => {
const date = new Date('2018-01-04T19:17:01.000Z');
const contents = [
'random line',
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const expected = [
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const target = path.join(basePath, 'log.log');
const files = [{
path: target,
}];
fs.writeFileSync(target, contents);
return eliminateOldEntries(files, date).then(() => {
expect(fs.readFileSync(target, 'utf8')).to.equal(`${expected}\n`);
});
});
it('preserves all lines if before target date', () => {
const date = new Date('2018-01-04T19:17:03.000Z');
const contents = [
'random line',
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const expected = [
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const target = path.join(basePath, 'log.log');
const files = [{
path: target,
}];
fs.writeFileSync(target, contents);
return eliminateOldEntries(files, date).then(() => {
expect(fs.readFileSync(target, 'utf8')).to.equal(`${expected}\n`);
});
});
});
describe('#fetchLog', () => {
it('returns error if file does not exist', () => {
const target = 'random_file';
return fetchLog(target).then(() => {
throw new Error('Expected an error!');
}, (error) => {
expect(error).to.have.property('message').that.match(/random_file/);
});
});
it('returns empty array if file has no valid JSON lines', () => {
const contents = 'line 1\nline2\n';
const expected = [];
const target = path.join(basePath, 'test.log');
fs.writeFileSync(target, contents);
return fetchLog(target).then((result) => {
expect(result).to.deep.equal(expected);
});
});
it('returns just three fields in each returned line', () => {
const contents = [
JSON.stringify({
one: 1,
two: 2,
level: 1,
time: 2,
msg: 3,
}),
JSON.stringify({
one: 1,
two: 2,
level: 2,
time: 3,
msg: 4,
}),
'',
].join('\n');
const expected = [{
level: 1,
time: 2,
msg: 3,
}, {
level: 2,
time: 3,
msg: 4,
}];
const target = path.join(basePath, 'test.log');
fs.writeFileSync(target, contents);
return fetchLog(target).then((result) => {
expect(result).to.deep.equal(expected);
});
});
});
describe('#fetch', () => {
it('returns single entry if no files', () => {
return fetch(basePath).then((results) => {
expect(results).to.have.length(1);
expect(results[0].msg).to.match(/Loaded this list/);
});
});
it('returns sorted entries from all files', () => {
const first = [
JSON.stringify({ msg: 2, time: '2018-01-04T19:17:05.014Z' }),
'',
].join('\n');
const second = [
JSON.stringify({ msg: 1, time: '2018-01-04T19:17:00.014Z' }),
JSON.stringify({ msg: 3, time: '2018-01-04T19:18:00.014Z' }),
'',
].join('\n');
fs.writeFileSync(path.join(basePath, 'first.log'), first);
fs.writeFileSync(path.join(basePath, 'second.log'), second);
return fetch(basePath).then((results) => {
expect(results).to.have.length(4);
expect(results[0].msg).to.equal(1);
expect(results[1].msg).to.equal(2);
expect(results[2].msg).to.equal(3);
});
});
});
});