diff --git a/CHANGELOG.md b/CHANGELOG.md index d31d585d2..e323b3353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.4.7 +--- +* Fixed `URIError: URI malformed` crash when `stringArray` with `base64`/`rc4` encoding processed a string literal containing lone surrogate code units (e.g. `"[^\uD800-\uDFFF]"`). Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1431 + v5.4.6 --- * Fixed unicode (`\uXXXX`, `\u{XXXX}`) and hex (`\xXX`) escape sequences of string literals being un-escaped into their literal characters during obfuscation. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/345 diff --git a/package.json b/package.json index ca70734b1..9ccf2b7d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.4.6", + "version": "5.4.7", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", diff --git a/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts b/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts index c6cce7c3b..3de30cd09 100644 --- a/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts +++ b/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts @@ -4,6 +4,7 @@ import { ServiceIdentifiers } from '../../container/ServiceIdentifiers'; import * as estraverse from '@javascript-obfuscator/estraverse'; import * as ESTree from 'estree'; +import { TStringArrayEncoding } from '../../types/options/TStringArrayEncoding'; import { TStringLiteralNode } from '../../types/node/TStringLiteralNode'; import { IOptions } from '../../interfaces/options/IOptions'; @@ -12,6 +13,8 @@ import { IStringArrayStorage } from '../../interfaces/storages/string-array-tran import { IStringArrayStorageAnalyzer } from '../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer'; import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-transformers/IStringArrayStorageItem'; +import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding'; + import { NodeGuards } from '../../node/NodeGuards'; import { NodeLiteralUtils } from '../../node/NodeLiteralUtils'; import { NodeMetadata } from '../../node/NodeMetadata'; @@ -26,6 +29,14 @@ export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer { */ private static readonly minimumLengthForStringArray: number = 3; + /** + * Matches a lone (unpaired) surrogate code unit. Because of the `u` flag, valid surrogate pairs are + * iterated as a single code point outside the `\uD800-\uDFFF` range, so only unpaired surrogates match. + * + * @type {RegExp} + */ + private static readonly loneSurrogateRegExp: RegExp = /[\uD800-\uDFFF]/u; + /** * @type {IOptions} */ @@ -128,6 +139,14 @@ export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer { * @returns {boolean} */ private shouldAddValueToStringArray(literalNode: TStringLiteralNode): boolean { + // `base64` and `rc4` encodings rely on `encodeURIComponent`/`decodeURIComponent`, which cannot + // represent lone (unpaired) surrogate code units. Keeping such values inline avoids a + // `URIError: URI malformed` crash while still producing valid obfuscated code. + // Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1431 + if (this.isProhibitedStringArrayValue(literalNode.value)) { + return false; + } + const isForceTransformNode: boolean = NodeMetadata.isForceTransformNode(literalNode); if (isForceTransformNode) { @@ -140,4 +159,17 @@ export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer { this.randomGenerator.getMathRandom() <= this.options.stringArrayThreshold ); } + + /** + * @param {string} value + * @returns {boolean} + */ + private isProhibitedStringArrayValue(value: string): boolean { + const hasUnicodeEncoding: boolean = this.options.stringArrayEncoding.some( + (encoding: TStringArrayEncoding): boolean => + encoding === StringArrayEncoding.Base64 || encoding === StringArrayEncoding.Rc4 + ); + + return hasUnicodeEncoding && StringArrayStorageAnalyzer.loneSurrogateRegExp.test(value); + } } diff --git a/test/functional-tests/issues/fixtures/issue1431.js b/test/functional-tests/issues/fixtures/issue1431.js new file mode 100644 index 000000000..266cbaf7c --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1431.js @@ -0,0 +1 @@ +const controlSymbolRegexString = "\\\\[^\uD800-\uDFFF]"; diff --git a/test/functional-tests/issues/issue1431.spec.ts b/test/functional-tests/issues/issue1431.spec.ts new file mode 100644 index 000000000..f81745a64 --- /dev/null +++ b/test/functional-tests/issues/issue1431.spec.ts @@ -0,0 +1,109 @@ +import { assert } from 'chai'; + +import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes'; + +import { StringArrayEncoding } from '../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding'; + +import { readFileAsString } from '../../helpers/readFileAsString'; + +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +// +// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1431 +// +describe('Issue #1431', () => { + describe('Base64/Rc4 string-array encoding of a literal with lone surrogate code units', () => { + describe('Variant #1: `base64` encoding with `splitStrings` (minimal reproduction)', () => { + let testFunc: () => string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1431.js'); + + testFunc = () => + JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayEncoding: [StringArrayEncoding.Base64], + stringArrayThreshold: 1, + splitStrings: true, + splitStringsChunkLength: 5, + seed: 1 + }).getObfuscatedCode(); + }); + + it('should not throw `URIError: URI malformed`', () => { + assert.doesNotThrow(testFunc); + }); + }); + + describe('Variant #2: `base64` encoding', () => { + let testFunc: () => string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1431.js'); + + testFunc = () => + JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayEncoding: [StringArrayEncoding.Base64], + stringArrayThreshold: 1, + seed: 1 + }).getObfuscatedCode(); + }); + + it('should not throw `URIError: URI malformed`', () => { + assert.doesNotThrow(testFunc); + }); + }); + + describe('Variant #3: `rc4` encoding', () => { + let testFunc: () => string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1431.js'); + + testFunc = () => + JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayEncoding: [StringArrayEncoding.Rc4], + stringArrayThreshold: 1, + seed: 1 + }).getObfuscatedCode(); + }); + + it('should not throw `URIError: URI malformed`', () => { + assert.doesNotThrow(testFunc); + }); + }); + }); + + describe('Obfuscated code stays runnable and preserves the original value', () => { + const originalValue: string = '\\\\[^\uD800-\uDFFF]'; + + let result: string; + + before(() => { + const code: string = 'module.exports = "\\\\\\\\[^\\uD800-\\uDFFF]";'; + + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayEncoding: [StringArrayEncoding.Base64], + stringArrayThreshold: 1, + seed: 1 + }).getObfuscatedCode(); + + const moduleObject: { exports: string } = { exports: '' }; + + new Function('module', obfuscatedCode)(moduleObject); + + result = moduleObject.exports; + }); + + it('should evaluate to the original string', () => { + assert.equal(result, originalValue); + }); + }); +});