From b53cf8a4808d020a01549ccad90629be2008b1f9 Mon Sep 17 00:00:00 2001 From: sanex Date: Sat, 5 Sep 2020 19:21:18 +0300 Subject: [PATCH] Test: try to fix unescape of escape sequences --- .../StringArrayStorageAnalyzer.ts | 9 +- .../StringArrayTransformer.ts | 14 ++- src/node/NodeLiteralUtils.ts | 14 +++ src/utils/EscapeSequenceEncoder.ts | 59 +++++++++- test/dev/dev.ts | 12 +- .../StringArrayTransformer.spec.ts | 106 ++++++++++++++++++ .../escaped-with-unicode-escape-sequence-1.js | 1 + .../escaped-with-unicode-escape-sequence-2.js | 1 + .../utils/EscapeSequenceEncoder.spec.ts | 50 ++++++++- 9 files changed, 244 insertions(+), 22 deletions(-) create mode 100644 test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/escaped-with-unicode-escape-sequence-1.js create mode 100644 test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/escaped-with-unicode-escape-sequence-2.js diff --git a/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts b/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts index ab0398257..d766ad3a8 100644 --- a/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts +++ b/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts @@ -111,10 +111,11 @@ export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer { return; } - this.stringArrayStorageData.set( - literalNode, - this.stringArrayStorage.getOrThrow(literalNode.value) - ); + // we have to use `raw` value here because it can contains unicode escape sequence + const key: string = NodeLiteralUtils.getUnwrappedLiteralNodeRawValue(literalNode); + const stringArrayStorageItemData: IStringArrayStorageItemData = this.stringArrayStorage.getOrThrow(key); + + this.stringArrayStorageData.set(literalNode, stringArrayStorageItemData); } /** diff --git a/src/node-transformers/string-array-transformers/StringArrayTransformer.ts b/src/node-transformers/string-array-transformers/StringArrayTransformer.ts index 6bf3d5f6a..74137a3d1 100644 --- a/src/node-transformers/string-array-transformers/StringArrayTransformer.ts +++ b/src/node-transformers/string-array-transformers/StringArrayTransformer.ts @@ -148,10 +148,11 @@ export class StringArrayTransformer extends AbstractNodeTransformer { } const literalValue: ESTree.SimpleLiteral['value'] = literalNode.value; + const literalRawValue: ESTree.SimpleLiteral['raw'] = literalNode.raw; const stringArrayStorageItemData: IStringArrayStorageItemData | undefined = this.stringArrayStorageAnalyzer .getItemDataForLiteralNode(literalNode); - const cacheKey: string = `${literalValue}-${Boolean(stringArrayStorageItemData)}`; + const cacheKey: string = `${literalRawValue}-${Boolean(stringArrayStorageItemData)}`; const useCachedValue: boolean = this.nodesCache.has(cacheKey) && stringArrayStorageItemData?.encoding !== StringArrayEncoding.Rc4; @@ -161,7 +162,7 @@ export class StringArrayTransformer extends AbstractNodeTransformer { const resultNode: ESTree.Node = stringArrayStorageItemData ? this.getStringArrayCallNode(stringArrayStorageItemData) - : this.getLiteralNode(literalValue); + : this.getLiteralNode(literalValue, literalRawValue ?? literalValue); this.nodesCache.set(cacheKey, resultNode); @@ -172,10 +173,11 @@ export class StringArrayTransformer extends AbstractNodeTransformer { /** * @param {string} value + * @param {string} rawValue * @returns {Node} */ - private getLiteralNode (value: string): ESTree.Node { - return NodeFactory.literalNode(value); + private getLiteralNode (value: string, rawValue: string): ESTree.Node { + return NodeFactory.literalNode(value, rawValue); } /** @@ -217,8 +219,10 @@ export class StringArrayTransformer extends AbstractNodeTransformer { return literalNode; } + const valueToEncode: string = NodeLiteralUtils.getUnwrappedLiteralNodeRawValue(literalNode); + return NodeFactory.literalNode( - this.escapeSequenceEncoder.encode(literalNode.value, this.options.unicodeEscapeSequence) + this.escapeSequenceEncoder.encode(valueToEncode, this.options.unicodeEscapeSequence) ); } } diff --git a/src/node/NodeLiteralUtils.ts b/src/node/NodeLiteralUtils.ts index 993f60e33..952e4753b 100644 --- a/src/node/NodeLiteralUtils.ts +++ b/src/node/NodeLiteralUtils.ts @@ -3,6 +3,20 @@ import * as ESTree from 'estree'; import { NodeGuards } from './NodeGuards'; export class NodeLiteralUtils { + /** + * @param {Literal} literalNode + * @returns {string} + */ + public static getUnwrappedLiteralNodeRawValue (literalNode: ESTree.Literal): string { + if (typeof literalNode.value !== 'string') { + throw new Error('Allowed only literal nodes with `string` values'); + } + + return literalNode.raw + ? literalNode.raw.slice(1, -1) + : literalNode.value; + } + /** * @param {Literal} literalNode * @param {Node} parentNode diff --git a/src/utils/EscapeSequenceEncoder.ts b/src/utils/EscapeSequenceEncoder.ts index b5ef08e7a..5de77b379 100644 --- a/src/utils/EscapeSequenceEncoder.ts +++ b/src/utils/EscapeSequenceEncoder.ts @@ -4,6 +4,16 @@ import { IEscapeSequenceEncoder } from '../interfaces/utils/IEscapeSequenceEncod @injectable() export class EscapeSequenceEncoder implements IEscapeSequenceEncoder { + /** + * @type {string} + */ + private static readonly unicodeLeadingCharacter: string = '\\u'; + + /** + * @type {string} + */ + private static readonly hexLeadingCharacter: string = '\\x'; + /** * @type {Map} */ @@ -21,6 +31,24 @@ export class EscapeSequenceEncoder implements IEscapeSequenceEncoder { return this.stringsCache.get(cacheKey); } + const encodedString: string = this.isEscapedString(string) + ? this.encodeEscapedString(string) + : this.encodeBaseString(string, encodeAllSymbols); + + this.stringsCache.set(cacheKey, encodedString); + this.stringsCache.set(`${encodedString}-${String(encodeAllSymbols)}`, encodedString); + + return encodedString; + } + + /** + * Base string + * + * @param {string} string + * @param {boolean} encodeAllSymbols + * @returns {string} + */ + public encodeBaseString (string: string, encodeAllSymbols: boolean): string { const radix: number = 16; const replaceRegExp: RegExp = new RegExp('[\\s\\S]', 'g'); const escapeSequenceRegExp: RegExp = new RegExp('[\'\"\\\\\\s]'); @@ -29,25 +57,44 @@ export class EscapeSequenceEncoder implements IEscapeSequenceEncoder { let prefix: string; let template: string; - const result: string = string.replace(replaceRegExp, (character: string): string => { + return string.replace(replaceRegExp, (character: string): string => { if (!encodeAllSymbols && !escapeSequenceRegExp.exec(character)) { return character; } if (regExp.exec(character)) { - prefix = '\\x'; + prefix = EscapeSequenceEncoder.hexLeadingCharacter; template = '00'; } else { - prefix = '\\u'; + prefix = EscapeSequenceEncoder.unicodeLeadingCharacter; template = '0000'; } return `${prefix}${(template + character.charCodeAt(0).toString(radix)).slice(-template.length)}`; }); + } + + /** + * String with escaped unicode escape sequence + * + * Example: + * \\ud83d\\ude03 + * + * @param {string} string + * @returns {string} + */ + public encodeEscapedString (string: string): string { + return string; + } - this.stringsCache.set(cacheKey, result); - this.stringsCache.set(`${result}-${String(encodeAllSymbols)}`, result); + /** + * @param {string} string + * @returns {boolean} + */ + private isEscapedString (string: string): boolean { + const unicodeLeadingCharacterRegExp: RegExp = new RegExp(`\\\\${EscapeSequenceEncoder.unicodeLeadingCharacter}`); + const hexLeadingCharacterRegExp: RegExp = new RegExp(`\\\\${EscapeSequenceEncoder.hexLeadingCharacter}`); - return result; + return unicodeLeadingCharacterRegExp.test(string) || hexLeadingCharacterRegExp.test(string); } } diff --git a/test/dev/dev.ts b/test/dev/dev.ts index cf7d31c72..eee7cafe0 100644 --- a/test/dev/dev.ts +++ b/test/dev/dev.ts @@ -7,18 +7,18 @@ import { NO_ADDITIONAL_NODES_PRESET } from '../../src/options/presets/NoCustomNo let obfuscatedCode: string = JavaScriptObfuscator.obfuscate( ` - var string1 = '👋🏼'; - - console.log(string1); + console.log("\\ud83d\\ude03\\ud83d\\ude03\\ud83d\\ude03"); + console.log("😃😃😃"); + console.log("abc efgx"); `, { ...NO_ADDITIONAL_NODES_PRESET, compact: false, - stringArray: true, + unicodeEscapeSequence: false, + stringArray: false, stringArrayThreshold: 1, splitStrings: true, - splitStringsChunkLength: 1, - unicodeEscapeSequence: true + splitStringsChunkLength: 1 } ).getObfuscatedCode(); diff --git a/test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/StringArrayTransformer.spec.ts b/test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/StringArrayTransformer.spec.ts index 0362754b9..e1fa5fb51 100644 --- a/test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/StringArrayTransformer.spec.ts +++ b/test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/StringArrayTransformer.spec.ts @@ -815,4 +815,110 @@ describe('StringArrayTransformer', function () { assert.match(obfuscatedCode, exportNamedDeclarationRegExp); }); }); + + describe('Variant #20: already escaped string with unicode escape sequence', () => { + describe('Variant #1: unicode escape sequence', () => { + describe('Variant #1: `unicodeEscapeSequence` option is disabled', () => { + const stringArrayRegExp: RegExp = /^var _0x([a-f0-9]){4} *= *\['\\ud83d\\ude03\\ud83d\\ude03\\ud83d\\ude03'];/; + + let obfuscatedCode: string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/escaped-with-unicode-escape-sequence-1.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate( + code, + { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayThreshold: 1, + unicodeEscapeSequence: false + + } + ).getObfuscatedCode(); + }); + + it('should keep unicode escape sequence on the string after adding to the string array', () => { + assert.match(obfuscatedCode, stringArrayRegExp); + }); + }); + + describe('Variant #2: `unicodeEscapeSequence` option is enabled', () => { + const stringArrayRegExp: RegExp = /^var _0x([a-f0-9]){4} *= *\['\\ud83d\\ude03\\ud83d\\ude03\\ud83d\\ude03'];/; + + let obfuscatedCode: string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/escaped-with-unicode-escape-sequence-1.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate( + code, + { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayThreshold: 1, + unicodeEscapeSequence: true + + } + ).getObfuscatedCode(); + }); + + it('should keep unicode escape sequence on the string after adding to the string array', () => { + assert.match(obfuscatedCode, stringArrayRegExp); + }); + }); + }); + + describe('Variant #2: hex escape sequence', () => { + describe('Variant #1: `unicodeEscapeSequence` option is disabled', () => { + const stringArrayRegExp: RegExp = /^var _0x([a-f0-9]){4} *= *\['\\x48\\x65\\x6c\\x6c\\x6f'];/; + + let obfuscatedCode: string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/escaped-with-unicode-escape-sequence-2.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate( + code, + { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayThreshold: 1, + unicodeEscapeSequence: false + + } + ).getObfuscatedCode(); + }); + + it('should keep unicode escape sequence on the string after adding to the string array', () => { + assert.match(obfuscatedCode, stringArrayRegExp); + }); + }); + + describe('Variant #2: `unicodeEscapeSequence` option is enabled', () => { + const stringArrayRegExp: RegExp = /^var _0x([a-f0-9]){4} *= *\['\\x48\\x65\\x6c\\x6c\\x6f'];/; + + let obfuscatedCode: string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/escaped-with-unicode-escape-sequence-2.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate( + code, + { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayThreshold: 1, + unicodeEscapeSequence: true + + } + ).getObfuscatedCode(); + }); + + it('should keep unicode escape sequence on the string after adding to the string array', () => { + assert.match(obfuscatedCode, stringArrayRegExp); + }); + }); + }); + }); }); diff --git a/test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/escaped-with-unicode-escape-sequence-1.js b/test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/escaped-with-unicode-escape-sequence-1.js new file mode 100644 index 000000000..b05744012 --- /dev/null +++ b/test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/escaped-with-unicode-escape-sequence-1.js @@ -0,0 +1 @@ +var test = '\ud83d\ude03\ud83d\ude03\ud83d\ude03'; diff --git a/test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/escaped-with-unicode-escape-sequence-2.js b/test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/escaped-with-unicode-escape-sequence-2.js new file mode 100644 index 000000000..1b79d465e --- /dev/null +++ b/test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/escaped-with-unicode-escape-sequence-2.js @@ -0,0 +1 @@ +var test = '\x48\x65\x6c\x6c\x6f'; diff --git a/test/unit-tests/utils/EscapeSequenceEncoder.spec.ts b/test/unit-tests/utils/EscapeSequenceEncoder.spec.ts index 925e09233..2b6afc547 100644 --- a/test/unit-tests/utils/EscapeSequenceEncoder.spec.ts +++ b/test/unit-tests/utils/EscapeSequenceEncoder.spec.ts @@ -1,3 +1,5 @@ +import 'reflect-metadata'; + import { assert } from 'chai'; import { InversifyContainerFacade } from '../../../src/container/InversifyContainerFacade'; @@ -6,7 +8,6 @@ import { ServiceIdentifiers } from '../../../src/container/ServiceIdentifiers'; import { IInversifyContainerFacade } from '../../../src/interfaces/container/IInversifyContainerFacade'; import { IEscapeSequenceEncoder } from '../../../src/interfaces/utils/IEscapeSequenceEncoder'; - describe('EscapeSequenceEncoder', () => { describe('encode', () => { let escapeSequenceEncoder: IEscapeSequenceEncoder; @@ -48,5 +49,52 @@ describe('EscapeSequenceEncoder', () => { assert.equal(actualString, expectedString); }); }); + + describe('Variant #3: ignore already escaped unicode string`', () => { + const string: string = '\\ud83d\\ude03'; + const expectedString: string = '\\ud83d\\ude03'; + + let actualString: string; + + before(() => { + actualString = escapeSequenceEncoder.encode(string, false); + }); + + it('should ignore already escaped string', () => { + assert.equal(actualString, expectedString); + }); + }); + + describe('Variant #4: ignore already escaped hex string`', () => { + describe('Variant #1: base`', () => { + const string: string = '\\x48\\x65\\x6c\\x6c\\x6f'; + const expectedString: string = '\\x48\\x65\\x6c\\x6c\\x6f'; + + let actualString: string; + + before(() => { + actualString = escapeSequenceEncoder.encode(string, true); + }); + + it('should ignore already escaped string', () => { + assert.equal(actualString, expectedString); + }); + }); + + describe('Variant #2: encode string with `x` character`', () => { + const string: string = 'xxx'; + const expectedString: string = '\\x78\\x78\\x78'; + + let actualString: string; + + before(() => { + actualString = escapeSequenceEncoder.encode(string, true); + }); + + it('should encode string with `x` character', () => { + assert.equal(actualString, expectedString); + }); + }); + }); }); });