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.
This commit is contained in:
parent
06a16baaa5
commit
a1ac810343
10 changed files with 409 additions and 27 deletions
|
@ -11,6 +11,10 @@ module.exports = {
|
||||||
'airbnb-base',
|
'airbnb-base',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
'more',
|
||||||
|
],
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
'comma-dangle': ['error', {
|
'comma-dangle': ['error', {
|
||||||
arrays: 'always-multiline',
|
arrays: 'always-multiline',
|
||||||
|
@ -29,6 +33,9 @@ module.exports = {
|
||||||
ignoreUrls: true,
|
ignoreUrls: true,
|
||||||
}],
|
}],
|
||||||
|
|
||||||
|
// encourage consistent use of `async` / `await` instead of `then`
|
||||||
|
'more/no-then': 'error',
|
||||||
|
|
||||||
// it helps readability to put public API at top,
|
// it helps readability to put public API at top,
|
||||||
'no-use-before-define': 'off',
|
'no-use-before-define': 'off',
|
||||||
|
|
||||||
|
@ -38,6 +45,7 @@ module.exports = {
|
||||||
// though we have a logger, we still remap console to log to disk
|
// though we have a logger, we still remap console to log to disk
|
||||||
'no-console': 'off',
|
'no-console': 'off',
|
||||||
|
|
||||||
|
// consistently place operators at end of line except ternaries
|
||||||
'operator-linebreak': 'error',
|
'operator-linebreak': 'error',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
|
||||||
|
/* eslint-disable more/no-then */
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
|
const isFunction = require('lodash/isFunction');
|
||||||
|
const isNumber = require('lodash/isNumber');
|
||||||
|
const isString = require('lodash/isString');
|
||||||
|
const isUndefined = require('lodash/isUndefined');
|
||||||
|
|
||||||
const MIME = require('./mime');
|
const MIME = require('./mime');
|
||||||
const { arrayBufferToBlob, blobToArrayBuffer, dataURLToBlob } = require('blob-util');
|
const { arrayBufferToBlob, blobToArrayBuffer, dataURLToBlob } = require('blob-util');
|
||||||
const { autoOrientImage } = require('../auto_orient_image');
|
const { autoOrientImage } = require('../auto_orient_image');
|
||||||
|
|
||||||
// Increment this everytime we change how attachments are upgraded. This allows us to
|
// Increment this version number every time we change how attachments are upgraded. This
|
||||||
// retroactively upgrade existing attachments. As we add more upgrade steps, we could
|
// will allow us to retroactively upgrade existing attachments. As we add more upgrade
|
||||||
// design a pipeline that does this incrementally, e.g. from version 0 (unknown) -> 1,
|
// steps, we could design a pipeline that does this incrementally, e.g. from
|
||||||
// 1 --> 2, etc., similar to how we do database migrations:
|
// version 0 / unknown -> 1, 1 --> 2, etc., similar to how we do database migrations:
|
||||||
const CURRENT_PROCESS_VERSION = 1;
|
exports.CURRENT_SCHEMA_VERSION = 2;
|
||||||
|
|
||||||
// Schema version history
|
// Schema version history
|
||||||
//
|
//
|
||||||
// Version 1
|
// Version 1
|
||||||
// - Auto-orient JPEG attachments using EXIF `Orientation` data
|
// - Auto-orient JPEG attachments using EXIF `Orientation` data
|
||||||
// - Add `schemaVersion` property
|
// - Add `schemaVersion` property
|
||||||
|
// Version 2
|
||||||
|
// - Sanitize Unicode order override characters
|
||||||
|
|
||||||
// // Incoming message attachment fields
|
// // Incoming message attachment fields
|
||||||
// {
|
// {
|
||||||
|
@ -37,26 +44,72 @@ const CURRENT_PROCESS_VERSION = 1;
|
||||||
// schemaVersion: integer
|
// schemaVersion: integer
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// Returns true if `rawAttachment` is a valid attachment based on our (limited)
|
||||||
|
// criteria. Over time, we can expand this definition to become more narrow:
|
||||||
|
exports.isValid = (rawAttachment) => {
|
||||||
|
// NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is
|
||||||
|
// deserialized by protobuf:
|
||||||
|
if (!rawAttachment) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isString(rawAttachment.contentType) &&
|
||||||
|
isString(rawAttachment.fileName);
|
||||||
|
};
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
// type UpgradeStep = Attachment -> Promise Attachment
|
// type UpgradeStep = Attachment -> Promise Attachment
|
||||||
|
|
||||||
// UpgradeStep -> SchemaVersion -> UpgradeStep
|
// SchemaVersion -> UpgradeStep -> UpgradeStep
|
||||||
const setSchemaVersion = (next, schemaVersion) => async (attachment) => {
|
exports.withSchemaVersion = (schemaVersion, upgrade) => {
|
||||||
|
if (!isNumber(schemaVersion)) {
|
||||||
|
throw new TypeError('`schemaVersion` must be a number');
|
||||||
|
}
|
||||||
|
if (!isFunction(upgrade)) {
|
||||||
|
throw new TypeError('`upgrade` must be a function');
|
||||||
|
}
|
||||||
|
|
||||||
|
return async (attachment) => {
|
||||||
|
if (!exports.isValid(attachment)) {
|
||||||
|
console.log('Attachment.withSchemaVersion: Invalid input attachment:', attachment);
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
const isAlreadyUpgraded = attachment.schemaVersion >= schemaVersion;
|
const isAlreadyUpgraded = attachment.schemaVersion >= schemaVersion;
|
||||||
if (isAlreadyUpgraded) {
|
if (isAlreadyUpgraded) {
|
||||||
return attachment;
|
return attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
let upgradedAttachment;
|
const expectedVersion = schemaVersion - 1;
|
||||||
try {
|
const isUnversioned = isUndefined(attachment.schemaVersion);
|
||||||
upgradedAttachment = await next(attachment);
|
const hasExpectedVersion = isUnversioned ||
|
||||||
} catch (error) {
|
attachment.schemaVersion === expectedVersion;
|
||||||
console.error('Attachment.setSchemaVersion: error:', error);
|
if (!hasExpectedVersion) {
|
||||||
upgradedAttachment = null;
|
console.log(
|
||||||
|
'WARNING: Attachment.withSchemaVersion: Unexpected version:' +
|
||||||
|
` Expected attachment to have version ${expectedVersion},` +
|
||||||
|
` but got ${attachment.schemaVersion}.`,
|
||||||
|
attachment
|
||||||
|
);
|
||||||
|
return attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasSuccessfullyUpgraded = upgradedAttachment !== null;
|
let upgradedAttachment;
|
||||||
if (!hasSuccessfullyUpgraded) {
|
try {
|
||||||
|
upgradedAttachment = await upgrade(attachment);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
'Attachment.withSchemaVersion: error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!exports.isValid(upgradedAttachment)) {
|
||||||
|
console.log(
|
||||||
|
'Attachment.withSchemaVersion: Invalid upgraded attachment:',
|
||||||
|
upgradedAttachment
|
||||||
|
);
|
||||||
return attachment;
|
return attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +119,7 @@ const setSchemaVersion = (next, schemaVersion) => async (attachment) => {
|
||||||
{ schemaVersion }
|
{ schemaVersion }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Upgrade steps
|
// Upgrade steps
|
||||||
const autoOrientJPEG = async (attachment) => {
|
const autoOrientJPEG = async (attachment) => {
|
||||||
|
@ -93,6 +147,39 @@ const autoOrientJPEG = async (attachment) => {
|
||||||
return newAttachment;
|
return newAttachment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UNICODE_LEFT_TO_RIGHT_OVERRIDE = '\u202D';
|
||||||
|
const UNICODE_RIGHT_TO_LEFT_OVERRIDE = '\u202E';
|
||||||
|
const UNICODE_REPLACEMENT_CHARACTER = '\uFFFD';
|
||||||
|
const INVALID_CHARACTERS_PATTERN = new RegExp(
|
||||||
|
`[${UNICODE_LEFT_TO_RIGHT_OVERRIDE}${UNICODE_RIGHT_TO_LEFT_OVERRIDE}]`,
|
||||||
|
'g'
|
||||||
|
);
|
||||||
|
// NOTE: Expose synchronous version to do property-based testing using `testcheck`,
|
||||||
|
// which currently doesn’t support async testing:
|
||||||
|
// https://github.com/leebyron/testcheck-js/issues/45
|
||||||
|
exports._replaceUnicodeOrderOverridesSync = (attachment) => {
|
||||||
|
if (!isString(attachment.fileName)) {
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedFilename = attachment.fileName.replace(
|
||||||
|
INVALID_CHARACTERS_PATTERN,
|
||||||
|
UNICODE_REPLACEMENT_CHARACTER
|
||||||
|
);
|
||||||
|
const newAttachment = Object.assign({}, attachment, {
|
||||||
|
fileName: normalizedFilename,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newAttachment;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.replaceUnicodeOrderOverrides = async attachment =>
|
||||||
|
exports._replaceUnicodeOrderOverridesSync(attachment);
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
|
const toVersion1 = exports.withSchemaVersion(1, autoOrientJPEG);
|
||||||
|
const toVersion2 = exports.withSchemaVersion(2, exports.replaceUnicodeOrderOverrides);
|
||||||
|
|
||||||
// UpgradeStep
|
// UpgradeStep
|
||||||
exports.upgradeSchema = setSchemaVersion(autoOrientJPEG, CURRENT_PROCESS_VERSION);
|
exports.upgradeSchema = async attachment =>
|
||||||
|
toVersion2(await toVersion1(attachment));
|
||||||
|
|
|
@ -143,6 +143,9 @@
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Temporarily allow `then` until we convert the entire file
|
||||||
|
// to `async` / `await`:
|
||||||
|
// eslint-disable-next-line more/no-then
|
||||||
window.autoOrientImage(file)
|
window.autoOrientImage(file)
|
||||||
.then(dataURL => this.addThumb(dataURL));
|
.then(dataURL => this.addThumb(dataURL));
|
||||||
break;
|
break;
|
||||||
|
@ -150,6 +153,9 @@
|
||||||
this.addThumb('images/file.svg'); break;
|
this.addThumb('images/file.svg'); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Temporarily allow `then` until we convert the entire file
|
||||||
|
// to `async` / `await`:
|
||||||
|
// eslint-disable-next-line more/no-then
|
||||||
this.autoScale(file).then(function(blob) {
|
this.autoScale(file).then(function(blob) {
|
||||||
var limitKb = 1000000;
|
var limitKb = 1000000;
|
||||||
var blobType = file.type === 'image/gif' ? 'gif' : type;
|
var blobType = file.type === 'image/gif' ? 'gif' : type;
|
||||||
|
@ -214,6 +220,9 @@
|
||||||
return newAttachment;
|
return newAttachment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NOTE: Temporarily allow `then` until we convert the entire file
|
||||||
|
// to `async` / `await`:
|
||||||
|
// eslint-disable-next-line more/no-then
|
||||||
return this.autoScale(file)
|
return this.autoScale(file)
|
||||||
.then(this.readFile)
|
.then(this.readFile)
|
||||||
.then(setFlags(attachmentFlags));
|
.then(setFlags(attachmentFlags));
|
||||||
|
|
3
main.js
3
main.js
|
@ -377,6 +377,8 @@ function showAbout() {
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
let ready = false;
|
let ready = false;
|
||||||
app.on('ready', () => {
|
app.on('ready', () => {
|
||||||
|
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
|
||||||
|
/* eslint-disable more/no-then */
|
||||||
let loggingSetupError;
|
let loggingSetupError;
|
||||||
logging.initialize().catch((error) => {
|
logging.initialize().catch((error) => {
|
||||||
loggingSetupError = error;
|
loggingSetupError = error;
|
||||||
|
@ -416,6 +418,7 @@ app.on('ready', () => {
|
||||||
const menu = Menu.buildFromTemplate(template);
|
const menu = Menu.buildFromTemplate(template);
|
||||||
Menu.setApplicationMenu(menu);
|
Menu.setApplicationMenu(menu);
|
||||||
});
|
});
|
||||||
|
/* eslint-enable more/no-then */
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', () => {
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"semver": "^5.4.1",
|
"semver": "^5.4.1",
|
||||||
"spellchecker": "^3.4.4",
|
"spellchecker": "^3.4.4",
|
||||||
|
"testcheck": "^1.0.0-rc.2",
|
||||||
"websocket": "^1.0.25"
|
"websocket": "^1.0.25"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -80,6 +81,7 @@
|
||||||
"eslint": "^4.14.0",
|
"eslint": "^4.14.0",
|
||||||
"eslint-config-airbnb-base": "^12.1.0",
|
"eslint-config-airbnb-base": "^12.1.0",
|
||||||
"eslint-plugin-import": "^2.8.0",
|
"eslint-plugin-import": "^2.8.0",
|
||||||
|
"eslint-plugin-more": "^0.3.1",
|
||||||
"extract-zip": "^1.6.6",
|
"extract-zip": "^1.6.6",
|
||||||
"grunt": "^1.0.1",
|
"grunt": "^1.0.1",
|
||||||
"grunt-cli": "^1.2.0",
|
"grunt-cli": "^1.2.0",
|
||||||
|
@ -92,6 +94,7 @@
|
||||||
"grunt-jscs": "^3.0.1",
|
"grunt-jscs": "^3.0.1",
|
||||||
"grunt-sass": "^2.0.0",
|
"grunt-sass": "^2.0.0",
|
||||||
"mocha": "^4.1.0",
|
"mocha": "^4.1.0",
|
||||||
|
"mocha-testcheck": "^1.0.0-rc.0",
|
||||||
"node-sass-import-once": "^1.2.0",
|
"node-sass-import-once": "^1.2.0",
|
||||||
"nyc": "^11.4.1",
|
"nyc": "^11.4.1",
|
||||||
"spectron": "^3.7.2",
|
"spectron": "^3.7.2",
|
||||||
|
|
6
test/modules/.eslintrc
Normal file
6
test/modules/.eslintrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"check": true,
|
||||||
|
"gen": true
|
||||||
|
}
|
||||||
|
}
|
246
test/modules/types/attachment_test.js
Normal file
246
test/modules/types/attachment_test.js
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
require('mocha-testcheck').install();
|
||||||
|
|
||||||
|
const { assert } = require('chai');
|
||||||
|
|
||||||
|
const Attachment = require('../../../js/modules/types/attachment');
|
||||||
|
|
||||||
|
describe('Attachment', () => {
|
||||||
|
describe('upgradeSchema', () => {
|
||||||
|
it('should upgrade an unversioned attachment to the latest version', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\u202Dfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\uFFFDfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: Attachment.CURRENT_SCHEMA_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Attachment.upgradeSchema(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
context('with multiple upgrade steps', () => {
|
||||||
|
it('should return last valid attachment when any upgrade step fails', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\u202Dfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\u202Dfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: 1,
|
||||||
|
hasUpgradedToVersion1: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const v1 = async attachment =>
|
||||||
|
Object.assign({}, attachment, { hasUpgradedToVersion1: true });
|
||||||
|
const v2 = async () => {
|
||||||
|
throw new Error('boom');
|
||||||
|
};
|
||||||
|
const v3 = async attachment =>
|
||||||
|
Object.assign({}, attachment, { hasUpgradedToVersion3: true });
|
||||||
|
|
||||||
|
const toVersion1 = Attachment.withSchemaVersion(1, v1);
|
||||||
|
const toVersion2 = Attachment.withSchemaVersion(2, v2);
|
||||||
|
const toVersion3 = Attachment.withSchemaVersion(3, v3);
|
||||||
|
|
||||||
|
const upgradeSchema = async attachment =>
|
||||||
|
toVersion3(await toVersion2(await toVersion1(attachment)));
|
||||||
|
|
||||||
|
const actual = await upgradeSchema(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip out-of-order upgrade steps', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\u202Dfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\u202Dfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: 2,
|
||||||
|
hasUpgradedToVersion1: true,
|
||||||
|
hasUpgradedToVersion2: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const v1 = async attachment =>
|
||||||
|
Object.assign({}, attachment, { hasUpgradedToVersion1: true });
|
||||||
|
const v2 = async attachment =>
|
||||||
|
Object.assign({}, attachment, { hasUpgradedToVersion2: true });
|
||||||
|
const v3 = async attachment =>
|
||||||
|
Object.assign({}, attachment, { hasUpgradedToVersion3: true });
|
||||||
|
|
||||||
|
const toVersion1 = Attachment.withSchemaVersion(1, v1);
|
||||||
|
const toVersion2 = Attachment.withSchemaVersion(2, v2);
|
||||||
|
const toVersion3 = Attachment.withSchemaVersion(3, v3);
|
||||||
|
|
||||||
|
// NOTE: We upgrade to 3 before 2, i.e. the pipeline should abort:
|
||||||
|
const upgradeSchema = async attachment =>
|
||||||
|
toVersion2(await toVersion3(await toVersion1(attachment)));
|
||||||
|
|
||||||
|
const actual = await upgradeSchema(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('withSchemaVersion', () => {
|
||||||
|
it('should require a version number', () => {
|
||||||
|
const toVersionX = () => {};
|
||||||
|
assert.throws(
|
||||||
|
() => Attachment.withSchemaVersion(toVersionX, 2),
|
||||||
|
'`schemaVersion` must be a number'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require an upgrade function', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => Attachment.withSchemaVersion(2, 3),
|
||||||
|
'`upgrade` must be a function'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip upgrading if attachment has already been upgraded', async () => {
|
||||||
|
const upgrade = async attachment =>
|
||||||
|
Object.assign({}, attachment, { foo: true });
|
||||||
|
const upgradeWithVersion = Attachment.withSchemaVersion(3, upgrade);
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
contentType: 'image/gif',
|
||||||
|
data: null,
|
||||||
|
fileName: 'foo.gif',
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: 4,
|
||||||
|
};
|
||||||
|
const actual = await upgradeWithVersion(input);
|
||||||
|
assert.deepEqual(actual, input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return original attachment if upgrade function throws', async () => {
|
||||||
|
const upgrade = async () => {
|
||||||
|
throw new Error('boom!');
|
||||||
|
};
|
||||||
|
const upgradeWithVersion = Attachment.withSchemaVersion(3, upgrade);
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
contentType: 'image/gif',
|
||||||
|
data: null,
|
||||||
|
fileName: 'foo.gif',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
const actual = await upgradeWithVersion(input);
|
||||||
|
assert.deepEqual(actual, input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return original attachment if upgrade function returns null', async () => {
|
||||||
|
const upgrade = async () => null;
|
||||||
|
const upgradeWithVersion = Attachment.withSchemaVersion(3, upgrade);
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
contentType: 'image/gif',
|
||||||
|
data: null,
|
||||||
|
fileName: 'foo.gif',
|
||||||
|
size: 1111,
|
||||||
|
};
|
||||||
|
const actual = await upgradeWithVersion(input);
|
||||||
|
assert.deepEqual(actual, input);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('replaceUnicodeOrderOverrides', () => {
|
||||||
|
it('should sanitize left-to-right order override character', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\u202Dfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: 1,
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\uFFFDfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sanitize right-to-left order override character', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\u202Efig.exe',
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: 1,
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\uFFFDfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sanitize multiple override characters', async () => {
|
||||||
|
const input = {
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\u202e\u202dlol\u202efig.exe',
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: 1,
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
data: null,
|
||||||
|
fileName: 'test\uFFFD\uFFFDlol\uFFFDfig.exe',
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasNoUnicodeOrderOverrides = value =>
|
||||||
|
!value.includes('\u202D') && !value.includes('\u202E');
|
||||||
|
|
||||||
|
check.it(
|
||||||
|
'should ignore non-order-override characters',
|
||||||
|
gen.string.suchThat(hasNoUnicodeOrderOverrides),
|
||||||
|
(fileName) => {
|
||||||
|
const input = {
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
data: null,
|
||||||
|
fileName,
|
||||||
|
size: 1111,
|
||||||
|
schemaVersion: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = Attachment._replaceUnicodeOrderOverridesSync(input);
|
||||||
|
assert.deepEqual(actual, input);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,3 +1,6 @@
|
||||||
|
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
|
||||||
|
/* eslint-disable more/no-then */
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -1661,6 +1661,10 @@ eslint-plugin-import@^2.8.0:
|
||||||
minimatch "^3.0.3"
|
minimatch "^3.0.3"
|
||||||
read-pkg-up "^2.0.0"
|
read-pkg-up "^2.0.0"
|
||||||
|
|
||||||
|
eslint-plugin-more@^0.3.1:
|
||||||
|
version "0.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-plugin-more/-/eslint-plugin-more-0.3.1.tgz#ff688fb3fa8f153c8bfd5d70c15a68dc222a1b31"
|
||||||
|
|
||||||
eslint-restricted-globals@^0.1.1:
|
eslint-restricted-globals@^0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7"
|
resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7"
|
||||||
|
@ -3468,6 +3472,12 @@ mksnapshot@^0.3.0:
|
||||||
fs-extra "0.26.7"
|
fs-extra "0.26.7"
|
||||||
request "^2.79.0"
|
request "^2.79.0"
|
||||||
|
|
||||||
|
mocha-testcheck@^1.0.0-rc.0:
|
||||||
|
version "1.0.0-rc.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mocha-testcheck/-/mocha-testcheck-1.0.0-rc.0.tgz#05e50203043be1537aef2a87dd96ccd447702773"
|
||||||
|
dependencies:
|
||||||
|
testcheck "^1.0.0-rc"
|
||||||
|
|
||||||
mocha@^4.1.0:
|
mocha@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794"
|
resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794"
|
||||||
|
@ -4982,6 +4992,10 @@ test-exclude@^4.1.1:
|
||||||
read-pkg-up "^1.0.1"
|
read-pkg-up "^1.0.1"
|
||||||
require-main-filename "^1.0.1"
|
require-main-filename "^1.0.1"
|
||||||
|
|
||||||
|
testcheck@^1.0.0-rc, testcheck@^1.0.0-rc.2:
|
||||||
|
version "1.0.0-rc.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/testcheck/-/testcheck-1.0.0-rc.2.tgz#11356a25b84575efe0b0857451e85b5fa74ee4e4"
|
||||||
|
|
||||||
text-table@~0.2.0:
|
text-table@~0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||||
|
|
Loading…
Add table
Reference in a new issue