diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e4ac0062..60ea00dd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ Change Log +v5.4.0 +--- +* Add support for `import attributes`. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1256 +* Replaced `mkdirp` dependency with native `fs.mkdirSync({ recursive: true })`. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1275. Thank you https://github.com/roli-lpci! + v5.3.1 --- * Fixed class expression name references inside class body being incorrectly resolved to an import binding with the same name, causing broken code at runtime. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1386 diff --git a/package.json b/package.json index ad496b54f..ecbf746c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.3.1", + "version": "5.4.0", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", @@ -21,10 +21,11 @@ }, "types": "typings/index.d.ts", "dependencies": { - "@javascript-obfuscator/escodegen": "2.3.1", + "@javascript-obfuscator/escodegen": "2.4.0", "@javascript-obfuscator/estraverse": "5.4.0", "@vercel/blob": ">=0.23.0", "acorn": "8.15.0", + "acorn-import-attributes": "^1.9.5", "assert": "2.1.0", "chalk": "4.1.2", "chance": "1.1.13", diff --git a/src/ASTParserFacade.ts b/src/ASTParserFacade.ts index 8e3c27bfb..ade6007c4 100644 --- a/src/ASTParserFacade.ts +++ b/src/ASTParserFacade.ts @@ -1,6 +1,9 @@ import * as acorn from 'acorn'; import * as ESTree from 'estree'; import chalk, { Chalk } from 'chalk'; +import { importAttributesOrAssertions } from 'acorn-import-attributes'; + +const AcornParser = acorn.Parser.extend(importAttributesOrAssertions); /** * Facade over AST parser `acorn` @@ -67,7 +70,7 @@ export class ASTParserFacade { sourceType }; - const program: acorn.Node & ESTree.Program = acorn.parse(sourceCode, config); + const program: acorn.Node & ESTree.Program = AcornParser.parse(sourceCode, config); if (comments.length) { program.comments = comments; diff --git a/src/declarations/acorn-import-attributes.d.ts b/src/declarations/acorn-import-attributes.d.ts new file mode 100644 index 000000000..76ea39ab9 --- /dev/null +++ b/src/declarations/acorn-import-attributes.d.ts @@ -0,0 +1,9 @@ +declare module 'acorn-import-attributes' { + import { Parser } from 'acorn'; + + type TParserExtension = (BaseParser: typeof Parser) => typeof Parser; + + export const importAttributes: TParserExtension; + export const importAssertions: TParserExtension; + export const importAttributesOrAssertions: TParserExtension; +} diff --git a/test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts b/test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts index 4955b44d8..d347675f9 100644 --- a/test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts +++ b/test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts @@ -1478,6 +1478,116 @@ describe('JavaScriptObfuscator', () => { }); }); + describe('Import attributes', () => { + describe('Variant #1: import with "with" syntax', () => { + const importAttributesRegExp: RegExp = /from\s*'\.\/config\.json'\s*with\s*\{\s*type\s*:\s*'json'\s*\}/; + + let obfuscatedCode: string; + + before(() => { + const code: string = `import config from './config.json' with { type: 'json' };\nconsole.log(config);`; + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should preserve import attributes in obfuscated code', () => { + assert.match(obfuscatedCode, importAttributesRegExp); + }); + }); + + describe('Variant #2: import with multiple attributes', () => { + const importAttributesRegExp: RegExp = /from\s*'\.\/data\.json'\s*with\s*\{\s*type\s*:\s*'json'\s*,\s*integrity\s*:\s*'sha384-abc'\s*\}/; + + let obfuscatedCode: string; + + before(() => { + const code: string = `import data from './data.json' with { type: 'json', integrity: 'sha384-abc' };\nconsole.log(data);`; + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should preserve multiple import attributes in obfuscated code', () => { + assert.match(obfuscatedCode, importAttributesRegExp); + }); + }); + + describe('Variant #3: import with "assert" syntax (deprecated)', () => { + const importAssertionsRegExp: RegExp = /from\s*'\.\/config\.json'\s*assert\s*\{\s*type\s*:\s*'json'\s*\}/; + + let obfuscatedCode: string; + + before(() => { + const code: string = `import config from './config.json' assert { type: 'json' };\nconsole.log(config);`; + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should preserve import assertions in obfuscated code', () => { + assert.match(obfuscatedCode, importAssertionsRegExp); + }); + }); + + describe('Variant #4: export with "with" syntax', () => { + const exportAttributesRegExp: RegExp = /from\s*'\.\/config\.json'\s*with\s*\{\s*type\s*:\s*'json'\s*\}/; + + let obfuscatedCode: string; + + before(() => { + const code: string = `export { default as config } from './config.json' with { type: 'json' };`; + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should preserve export attributes in obfuscated code', () => { + assert.match(obfuscatedCode, exportAttributesRegExp); + }); + }); + + describe('Variant #5: export all with "with" syntax', () => { + const exportAllAttributesRegExp: RegExp = /export\s*\*\s*from\s*'\.\/utils\.js'\s*with\s*\{\s*type\s*:\s*'javascript'\s*\}/; + + let obfuscatedCode: string; + + before(() => { + const code: string = `export * from './utils.js' with { type: 'javascript' };`; + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should preserve export all attributes in obfuscated code', () => { + assert.match(obfuscatedCode, exportAllAttributesRegExp); + }); + }); + + describe('Variant #6: side-effect import with "with" syntax', () => { + const sideEffectImportRegExp: RegExp = /import\s*'\.\/styles\.css'\s*with\s*\{\s*type\s*:\s*'css'\s*\}/; + + let obfuscatedCode: string; + + before(() => { + const code: string = `import './styles.css' with { type: 'css' };`; + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should preserve side-effect import attributes in obfuscated code', () => { + assert.match(obfuscatedCode, sideEffectImportRegExp); + }); + }); + }); + describe('getOptionsByPreset', () => { describe('Variant #1: base behaviour', () => { const optionsPresetName: TOptionsPreset = OptionsPreset.HighObfuscation; diff --git a/test/unit-tests/javascript-obfuscator/ASTParserFacade.spec.ts b/test/unit-tests/javascript-obfuscator/ASTParserFacade.spec.ts index 2546cebea..44323483b 100644 --- a/test/unit-tests/javascript-obfuscator/ASTParserFacade.spec.ts +++ b/test/unit-tests/javascript-obfuscator/ASTParserFacade.spec.ts @@ -87,5 +87,79 @@ describe('ASTParserFacade', () => { }); }); }); + + describe('Import attributes', () => { + describe('Variant #1: import with "with" syntax', () => { + const sourceCode: string = `import config from "./config.json" with { type: "json" };`; + + let astTree: any; + + before(() => { + astTree = ASTParserFacade.parse(sourceCode, { ecmaVersion }); + }); + + it('should parse import with "with" syntax without error', () => { + assert.exists(astTree); + assert.equal(astTree.type, 'Program'); + assert.equal(astTree.body[0].type, 'ImportDeclaration'); + }); + + it('should preserve import attributes in AST', () => { + const importDecl = astTree.body[0]; + assert.exists(importDecl.attributes); + assert.isArray(importDecl.attributes); + assert.equal(importDecl.attributes.length, 1); + assert.equal(importDecl.attributes[0].key.name, 'type'); + assert.equal(importDecl.attributes[0].value.value, 'json'); + }); + }); + + describe('Variant #2: import with "assert" syntax (deprecated)', () => { + const sourceCode: string = `import config from "./config.json" assert { type: "json" };`; + + let astTree: any; + + before(() => { + astTree = ASTParserFacade.parse(sourceCode, { ecmaVersion }); + }); + + it('should parse import with "assert" syntax without error', () => { + assert.exists(astTree); + assert.equal(astTree.type, 'Program'); + assert.equal(astTree.body[0].type, 'ImportDeclaration'); + }); + }); + + describe('Variant #3: export with "with" syntax', () => { + const sourceCode: string = `export { default as config } from "./config.json" with { type: "json" };`; + + let astTree: any; + + before(() => { + astTree = ASTParserFacade.parse(sourceCode, { ecmaVersion }); + }); + + it('should parse export with "with" syntax without error', () => { + assert.exists(astTree); + assert.equal(astTree.type, 'Program'); + assert.equal(astTree.body[0].type, 'ExportNamedDeclaration'); + }); + }); + + describe('Variant #4: dynamic import with "with" syntax', () => { + const sourceCode: string = `const config = await import("./config.json", { with: { type: "json" } });`; + + let astTree: any; + + before(() => { + astTree = ASTParserFacade.parse(sourceCode, { ecmaVersion }); + }); + + it('should parse dynamic import with "with" syntax without error', () => { + assert.exists(astTree); + assert.equal(astTree.type, 'Program'); + }); + }); + }); }); }); diff --git a/yarn.lock b/yarn.lock index 79f0d7111..36a02aaed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -329,10 +329,10 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@javascript-obfuscator/escodegen@2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@javascript-obfuscator/escodegen/-/escodegen-2.3.1.tgz#a534e73740830d6c7546ca686773b40b09a6b9d1" - integrity sha512-Z0HEAVwwafOume+6LFXirAVZeuEMKWuPzpFbQhCEU9++BMz0IwEa9bmedJ+rMn/IlXRBID9j3gQ0XYAa6jM10g== +"@javascript-obfuscator/escodegen@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@javascript-obfuscator/escodegen/-/escodegen-2.4.0.tgz#9d2b19b94793106cafa5cd5041c4e40d42943fb7" + integrity sha512-h9cJ/qb3Y3c1jMQPWypt2CGTFgP34V5OtWLqoOCjV6CT/DUXMZFpoTAfDHpuUrRP0oxNd0UwnVAsPtPuYsoXxQ== dependencies: "@javascript-obfuscator/estraverse" "^5.3.0" esprima "^4.0.1" @@ -1017,6 +1017,11 @@ abbrev@^2.0.0: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + acorn-import-phases@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7"