signal-desktop/test/server/app/logging_test.js
Daniel Gasienica a1ac810343 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

    🎨 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 13:21:53 -05:00

274 lines
8.8 KiB
JavaScript

// 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);
});
});
});
});