From 4ac336812206d92bdcd9542b1b1f88881fce290b Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Sun, 7 Dec 2025 12:42:47 +0400 Subject: [PATCH 01/43] Version 5.0.1 (#1344) --- .github/workflows/ci.yml | 5 +- CHANGELOG.md | 4 + CLAUDE.md | 3 +- README.md | 1 - package.json | 12 +- src/JavaScriptObfuscator.ts | 6 + src/enums/logger/LoggingMessage.ts | 6 +- src/interfaces/logger/ILogger.ts | 6 + src/logger/Logger.ts | 7 + src/pro-api/constants.ts | 5 + src/utils/AdvertisementUtils.ts | 228 ++++++++++++++++++ test/index.spec.ts | 1 + .../utils/AdvertisementUtils.spec.ts | 213 ++++++++++++++++ yarn.lock | 102 +++++++- 14 files changed, 576 insertions(+), 23 deletions(-) create mode 100644 src/pro-api/constants.ts create mode 100644 src/utils/AdvertisementUtils.ts create mode 100644 test/unit-tests/utils/AdvertisementUtils.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bb2352bf..635547b33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: JavaScript Obfuscator CI on: push: - branches: [master] + branches: [master, release-**] pull_request: - branches: [master] + branches: [master, release-**] schedule: - cron: '0 1 * * *' @@ -44,6 +44,7 @@ jobs: key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - run: yarn install - run: yarn run build + - run: yarn run test:mocha-coverage - run: yarn run test:mocha-coverage:report - name: Coveralls uses: coverallsapp/github-action@master diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f00f9e9..359185db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.0.1 +--- +* Add JavaScript Obfuscator PRO advertisement message + v5.0.0 --- * Add JavaScript Obfuscator PRO support via calling its API diff --git a/CLAUDE.md b/CLAUDE.md index e55ec6c9c..3236bb57e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ **JavaScript Obfuscator** is a powerful, enterprise-grade code obfuscation tool for JavaScript and Node.js applications. It transforms readable JavaScript code into a protected, difficult-to-understand format while maintaining full functionality. The project is widely used for protecting intellectual property and preventing reverse engineering. -- **Version**: 4.1.1 +- **Version**: 5.0.0 - **Author**: Timofey Kachalov (@sanex3339) - **License**: BSD-2-Clause - **Repository**: https://github.com/javascript-obfuscator/javascript-obfuscator @@ -1423,7 +1423,6 @@ Use [grunt-contrib-obfuscator](https://github.com/javascript-obfuscator/grunt-co - **GitHub Issues**: Bug reports and feature requests - **GitHub Discussions**: Questions and general discussion -- **OpenCollective**: Financial support and sponsorship - **GitHub Sponsors**: Direct sponsorship ## License diff --git a/README.md b/README.md index 04313a8a5..5c2c4a38c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ #### You can support this project by donating: * (Github) https://github.com/sponsors/sanex3339 -* (OpenCollective) https://opencollective.com/javascript-obfuscator Huge thanks to all supporters! diff --git a/package.json b/package.json index 197ba0f58..9e91a042e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.0.0", + "version": "5.0.1", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", @@ -29,6 +29,7 @@ "chance": "1.1.13", "class-validator": "0.14.3", "commander": "12.1.0", + "conf": "15.0.2", "eslint-scope": "8.4.0", "eslint-visitor-keys": "4.2.1", "fast-deep-equal": "3.1.3", @@ -37,7 +38,6 @@ "md5": "2.3.0", "mkdirp": "3.0.1", "multimatch": "5.0.0", - "opencollective-postinstall": "2.0.3", "process": "0.11.10", "reflect-metadata": "0.2.2", "source-map-support": "0.5.21", @@ -124,7 +124,6 @@ "prettier:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"", "format": "yarn run prettier && yarn run eslint --fix", "git:addFiles": "git add .", - "postinstall": "opencollective-postinstall", "precommit": "yarn run eslint", "prepublishOnly": "yarn run build && yarn run build:typings", "prepare": "husky install" @@ -137,12 +136,5 @@ "Dmitry Zamotkin (https://github.com/zamotkin)" ], "license": "BSD-2-Clause", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/javascript-obfuscator" - }, - "collective": { - "url": "https://opencollective.com/javascript-obfuscator" - }, "packageManager": "yarn@1.22.21+sha512.ca75da26c00327d26267ce33536e5790f18ebd53266796fbb664d2a4a5116308042dd8ee7003b276a20eace7d3c5561c3577bdd71bcb67071187af124779620a" } diff --git a/src/JavaScriptObfuscator.ts b/src/JavaScriptObfuscator.ts index f6f91a5dd..53381b977 100644 --- a/src/JavaScriptObfuscator.ts +++ b/src/JavaScriptObfuscator.ts @@ -28,6 +28,7 @@ import { ecmaVersion } from './constants/EcmaVersion'; import { ASTParserFacade } from './ASTParserFacade'; import { NodeGuards } from './node/NodeGuards'; import { Utils } from './utils/Utils'; +import { AdvertisementUtils } from './utils/AdvertisementUtils'; @injectable() export class JavaScriptObfuscator implements IJavaScriptObfuscator { @@ -157,6 +158,11 @@ export class JavaScriptObfuscator implements IJavaScriptObfuscator { * @returns {IObfuscationResult} */ public obfuscate(sourceCode: string): IObfuscationResult { + if (AdvertisementUtils.shouldShowAdvertisement()) { + this.logger.advertise(LoggingMessage.JavaScriptObfuscatorProAdFirstPart); + this.logger.advertise(LoggingMessage.JavaScriptObfuscatorProAdSecondPart); + } + if (typeof sourceCode !== 'string') { sourceCode = ''; } diff --git a/src/enums/logger/LoggingMessage.ts b/src/enums/logger/LoggingMessage.ts index 2a2ebe6af..1e7eaaa54 100644 --- a/src/enums/logger/LoggingMessage.ts +++ b/src/enums/logger/LoggingMessage.ts @@ -1,3 +1,5 @@ +import { proAdvertiseMessageFirstPart, proAdvertiseMessageSecondPart } from '../../pro-api/constants'; + export enum LoggingMessage { EmptySourceCode = 'Empty source code. Obfuscation canceled...', ObfuscationCompleted = 'Obfuscation completed. Total time: %s sec.', @@ -5,5 +7,7 @@ export enum LoggingMessage { RandomGeneratorSeed = 'Random generator seed: %s...', CodeTransformationStage = 'Code transformation stage: %s...', NodeTransformationStage = 'AST transformation stage: %s...', - Version = 'Version: %s' + Version = 'Version: %s', + JavaScriptObfuscatorProAdFirstPart = proAdvertiseMessageFirstPart, + JavaScriptObfuscatorProAdSecondPart = proAdvertiseMessageSecondPart } diff --git a/src/interfaces/logger/ILogger.ts b/src/interfaces/logger/ILogger.ts index f186f5029..3977ae814 100644 --- a/src/interfaces/logger/ILogger.ts +++ b/src/interfaces/logger/ILogger.ts @@ -18,4 +18,10 @@ export interface ILogger { * @param {string | number} value */ warn(loggingMessage: LoggingMessage, value?: string | number): void; + + /** + * @param {LoggingMessage} loggingMessage + * @param {string | number} value + */ + advertise(loggingMessage: LoggingMessage): void; } diff --git a/src/logger/Logger.ts b/src/logger/Logger.ts index 5b706cb16..31b07ed98 100644 --- a/src/logger/Logger.ts +++ b/src/logger/Logger.ts @@ -90,4 +90,11 @@ export class Logger implements ILogger { Logger.log(Logger.colorWarn, LoggingPrefix.Base, loggingMessage, value); } + + /** + * @param {LoggingMessage} loggingMessage + */ + public advertise(loggingMessage: LoggingMessage): void { + Logger.log(Logger.colorInfo, LoggingPrefix.Base, loggingMessage); + } } diff --git a/src/pro-api/constants.ts b/src/pro-api/constants.ts new file mode 100644 index 000000000..3a69e4f9e --- /dev/null +++ b/src/pro-api/constants.ts @@ -0,0 +1,5 @@ +export const proAdvertiseMessageFirstPart = `πŸ›‘οΈ JavaScript Obfuscator Pro is now available β€” with powerful Virtual Machine-based obfuscation +(bytecode virtualization, anti-decompilation, unique opcode and VM structure each compilation, and more).`; + +export const proAdvertiseMessageSecondPart = + 'πŸ‘‰οΈ Get your API key at https://obfuscator.io and start using Virtual Machine obfuscation with javascript-obfuscator package.'; diff --git a/src/utils/AdvertisementUtils.ts b/src/utils/AdvertisementUtils.ts new file mode 100644 index 000000000..f9c5c6008 --- /dev/null +++ b/src/utils/AdvertisementUtils.ts @@ -0,0 +1,228 @@ +/** + * Utility class for managing PRO advertisement display + * - Limits display to first N times + * - Skips display in CI environments + * - Only works in Node.js (returns false in browser) + */ +export class AdvertisementUtils { + /** + * Maximum number of times to show the advertisement + */ + private static readonly maxDisplayCount: number = 5; + + /** + * Storage key for the display count + */ + private static readonly storageKey: string = 'adDisplayCount'; + + /** + * Storage key for the timestamp of first display + */ + private static readonly timestampKey: string = 'adFirstDisplayTime'; + + /** + * Reset period in milliseconds (3 days) + */ + private static readonly resetPeriodMs: number = 3 * 24 * 60 * 60 * 1000; + + /** + * Common CI environment variables to detect + */ + private static readonly ciEnvVars: string[] = [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'TRAVIS', + 'CIRCLECI', + 'JENKINS_URL', + 'HUDSON_URL', + 'TEAMCITY_VERSION', + 'BUILDKITE', + 'TF_BUILD', // Azure Pipelines + 'BITBUCKET_BUILD_NUMBER', + 'CODEBUILD_BUILD_ID', // AWS CodeBuild + 'DRONE', + 'HEROKU_TEST_RUN_ID', + 'NETLIFY', + 'VERCEL', + 'NOW_BUILDER', // Vercel (legacy) + 'RENDER', + 'CODESANDBOX_SSE', + 'STACKBLITZ' + ]; + + /** + * Cached conf instance + */ + private static config: any = null; + + /** + * Check if running in a CI environment + */ + public static isCI(): boolean { + if (!this.isNodeEnvironment()) { + return false; + } + + return this.ciEnvVars.some((envVar) => { + const value = process.env[envVar]; + + return value !== undefined && value !== '' && value !== '0' && value !== 'false'; + }); + } + + /** + * Check if advertisement should be displayed + * Returns true if: + * - Running in Node.js (not browser) + * - Not in CI environment + * - Display count is less than maxDisplayCount + * + * Also increments the display count if returning true + * + * In browser environments, always returns false + */ + public static shouldShowAdvertisement(): boolean { + // Don't show in browser - only Node.js CLI + if (!this.isNodeEnvironment()) { + return false; + } + + // Don't show in CI environments + if (this.isCI()) { + return false; + } + + if (!process.stdout.isTTY) { + return false; + } + + // Initialize config if needed + const config = this.getConfig(); + + if (!config) { + return false; + } + + // Check if reset period has passed (3 days) + const firstDisplayTime = this.getFirstDisplayTime(config); + const now = Date.now(); + + if (firstDisplayTime && now - firstDisplayTime >= this.resetPeriodMs) { + // Reset counter after 3 days + this.resetDisplayData(config); + } + + // Check display count + const count = this.getDisplayCount(config); + + if (count >= this.maxDisplayCount) { + return false; + } + + // Set first display time if not set + if (!firstDisplayTime || now - firstDisplayTime >= this.resetPeriodMs) { + this.setFirstDisplayTime(config, now); + } + + // Increment count for next time + this.setDisplayCount(config, count + 1); + + return true; + } + + /** + * Check if running in Node.js environment + */ + private static isNodeEnvironment(): boolean { + return typeof process !== 'undefined' && process.versions?.node !== undefined; + } + + /** + * Get or create conf instance + */ + private static getConfig(): any { + if (this.config) { + return this.config; + } + + if (typeof window === 'undefined') { + try { + // Dynamic import to avoid bundling in browser + // eslint-disable-next-line no-eval + const Conf = eval('require')('conf').default; + + this.config = new Conf({ + projectName: 'javascript-obfuscator' + }); + + return this.config; + } catch { + return null; + } + } + + return null; + } + + /** + * Get current display count from config + */ + private static getDisplayCount(config: any): number { + try { + const count = config.get(this.storageKey, 0); + + return typeof count === 'number' ? count : 0; + } catch { + return 0; + } + } + + /** + * Set display count in config + */ + private static setDisplayCount(config: any, count: number): void { + try { + config.set(this.storageKey, count); + } catch { + // Ignore errors + } + } + + /** + * Get first display timestamp from config + */ + private static getFirstDisplayTime(config: any): number | null { + try { + const timestamp = config.get(this.timestampKey, null); + + return typeof timestamp === 'number' ? timestamp : null; + } catch { + return null; + } + } + + /** + * Set first display timestamp in config + */ + private static setFirstDisplayTime(config: any, timestamp: number): void { + try { + config.set(this.timestampKey, timestamp); + } catch { + // Ignore errors + } + } + + /** + * Reset display data (count and timestamp) + */ + private static resetDisplayData(config: any): void { + try { + config.delete(this.storageKey); + config.delete(this.timestampKey); + } catch { + // Ignore errors + } + } +} diff --git a/test/index.spec.ts b/test/index.spec.ts index 4c585766c..f156f832e 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -42,6 +42,7 @@ import './unit-tests/storages/identifier-names-cache/PropertyIdentifierNamesCach import './unit-tests/storages/string-array-transformers/literal-nodes-cache/LiteralNodesCacheStorage.spec'; import './unit-tests/storages/string-array-transformers/string-array/StringArrayStorage.spec'; import './unit-tests/storages/string-array-transformers/visited-lexical-scope-nodes-stack/VisitedLexicalScopeNodesStackStorage.spec'; +import './unit-tests/utils/AdvertisementUtils.spec'; import './unit-tests/utils/ArrayUtils.spec'; import './unit-tests/utils/CryptUtils.spec'; import './unit-tests/utils/CryptUtilsStringArray.spec'; diff --git a/test/unit-tests/utils/AdvertisementUtils.spec.ts b/test/unit-tests/utils/AdvertisementUtils.spec.ts new file mode 100644 index 000000000..b110e2f19 --- /dev/null +++ b/test/unit-tests/utils/AdvertisementUtils.spec.ts @@ -0,0 +1,213 @@ +import { assert } from 'chai'; + +import { AdvertisementUtils } from '../../../src/utils/AdvertisementUtils'; + +describe('AdvertisementUtils', () => { + describe('isCI', () => { + const originalEnv = { ...process.env }; + + afterEach(() => { + // Restore original environment + process.env = { ...originalEnv }; + }); + + describe('Variant #1: CI environment variable set', () => { + it('should return true when CI=true', () => { + process.env.CI = 'true'; + assert.isTrue(AdvertisementUtils.isCI()); + }); + + it('should return true when CI=1', () => { + process.env.CI = '1'; + assert.isTrue(AdvertisementUtils.isCI()); + }); + + it('should return true when GITHUB_ACTIONS is set', () => { + process.env.GITHUB_ACTIONS = 'true'; + assert.isTrue(AdvertisementUtils.isCI()); + }); + + it('should return true when TRAVIS is set', () => { + process.env.TRAVIS = 'true'; + assert.isTrue(AdvertisementUtils.isCI()); + }); + + it('should return true when GITLAB_CI is set', () => { + process.env.GITLAB_CI = 'true'; + assert.isTrue(AdvertisementUtils.isCI()); + }); + + it('should return true when JENKINS_URL is set', () => { + process.env.JENKINS_URL = 'http://jenkins.example.com'; + assert.isTrue(AdvertisementUtils.isCI()); + }); + }); + + describe('Variant #2: CI environment variable not set or false', () => { + beforeEach(() => { + // Clear all CI-related env vars + delete process.env.CI; + delete process.env.CONTINUOUS_INTEGRATION; + delete process.env.GITHUB_ACTIONS; + delete process.env.GITLAB_CI; + delete process.env.TRAVIS; + delete process.env.CIRCLECI; + delete process.env.JENKINS_URL; + delete process.env.BUILDKITE; + delete process.env.TF_BUILD; + }); + + it('should return false when no CI env vars are set', () => { + assert.isFalse(AdvertisementUtils.isCI()); + }); + + it('should return false when CI=false', () => { + process.env.CI = 'false'; + assert.isFalse(AdvertisementUtils.isCI()); + }); + + it('should return false when CI=0', () => { + process.env.CI = '0'; + assert.isFalse(AdvertisementUtils.isCI()); + }); + + it('should return false when CI is empty string', () => { + process.env.CI = ''; + assert.isFalse(AdvertisementUtils.isCI()); + }); + }); + }); + + describe('shouldShowAdvertisement', () => { + const originalEnv = { ...process.env }; + const originalIsTTY = process.stdout.isTTY; + + afterEach(() => { + process.env = { ...originalEnv }; + process.stdout.isTTY = originalIsTTY; + }); + + describe('Variant #1: non-TTY environment', () => { + it('should return false when stdout is not a TTY', () => { + process.stdout.isTTY = false; + // Clear CI env vars + delete process.env.CI; + assert.isFalse(AdvertisementUtils.shouldShowAdvertisement()); + }); + }); + + describe('Variant #2: CI environment', () => { + it('should return false in CI environment', () => { + process.stdout.isTTY = true; + process.env.CI = 'true'; + assert.isFalse(AdvertisementUtils.shouldShowAdvertisement()); + }); + }); + + describe('Variant #3: display counter and reset', () => { + let config: any; + + before(() => { + // Get config instance using eval('require') - same as AdvertisementUtils + // conf is an ES Module, so we need to access .default + // eslint-disable-next-line no-eval + const Conf = eval('require')('conf').default; + config = new Conf({ projectName: 'javascript-obfuscator' }); + }); + + beforeEach(() => { + // Clear ad-related config before each test + config.delete('adDisplayCount'); + config.delete('adFirstDisplayTime'); + // Reset cached config in AdvertisementUtils + (AdvertisementUtils as any).config = null; + // Ensure TTY and non-CI environment + process.stdout.isTTY = true; + delete process.env.CI; + delete process.env.GITHUB_ACTIONS; + delete process.env.TRAVIS; + delete process.env.GITLAB_CI; + }); + + afterEach(() => { + // Clean up + config.delete('adDisplayCount'); + config.delete('adFirstDisplayTime'); + (AdvertisementUtils as any).config = null; + }); + + it('should return true for first 5 calls', () => { + for (let i = 0; i < 5; i++) { + assert.isTrue(AdvertisementUtils.shouldShowAdvertisement(), `Call ${i + 1} should return true`); + } + }); + + it('should return false after 5 calls', () => { + // Exhaust the counter + for (let i = 0; i < 5; i++) { + AdvertisementUtils.shouldShowAdvertisement(); + } + + // 6th call should return false + assert.isFalse(AdvertisementUtils.shouldShowAdvertisement()); + }); + + it('should increment counter on each call', () => { + AdvertisementUtils.shouldShowAdvertisement(); + assert.strictEqual(config.get('adDisplayCount'), 1); + + AdvertisementUtils.shouldShowAdvertisement(); + assert.strictEqual(config.get('adDisplayCount'), 2); + + AdvertisementUtils.shouldShowAdvertisement(); + assert.strictEqual(config.get('adDisplayCount'), 3); + }); + + it('should set first display timestamp on first call', () => { + const beforeTime = Date.now(); + AdvertisementUtils.shouldShowAdvertisement(); + const afterTime = Date.now(); + + const timestamp = config.get('adFirstDisplayTime'); + assert.isNumber(timestamp); + assert.isAtLeast(timestamp, beforeTime); + assert.isAtMost(timestamp, afterTime); + }); + + it('should reset counter after 3 days', () => { + // Exhaust counter + for (let i = 0; i < 5; i++) { + AdvertisementUtils.shouldShowAdvertisement(); + } + assert.isFalse(AdvertisementUtils.shouldShowAdvertisement()); + + // Simulate 3 days passing by setting old timestamp + const threeDaysAgo = Date.now() - 3 * 24 * 60 * 60 * 1000 - 1000; + config.set('adFirstDisplayTime', threeDaysAgo); + // Reset cached config + (AdvertisementUtils as any).config = null; + + // Should return true again after reset + assert.isTrue(AdvertisementUtils.shouldShowAdvertisement()); + // Counter should be reset to 1 + assert.strictEqual(config.get('adDisplayCount'), 1); + }); + + it('should not reset counter before 3 days', () => { + // Exhaust counter + for (let i = 0; i < 5; i++) { + AdvertisementUtils.shouldShowAdvertisement(); + } + + // Simulate 2 days passing (less than 3 days) + const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1000; + config.set('adFirstDisplayTime', twoDaysAgo); + // Reset cached config + (AdvertisementUtils as any).config = null; + + // Should still return false + assert.isFalse(AdvertisementUtils.shouldShowAdvertisement()); + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 93f02ef83..c8a36b504 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1053,6 +1053,13 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" +ajv-formats@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== + dependencies: + ajv "^8.0.0" + ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" @@ -1085,7 +1092,7 @@ ajv@^8.0.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ajv@^8.9.0: +ajv@^8.17.1, ajv@^8.9.0: version "8.17.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== @@ -1273,6 +1280,14 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +atomically@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/atomically/-/atomically-2.1.0.tgz#5a3ce8ea5ab57b65df589a3b63ef7b753cc0af07" + integrity sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q== + dependencies: + stubborn-fs "^2.0.0" + when-exit "^2.1.4" + available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz" @@ -1622,6 +1637,21 @@ concat-map@0.0.1: resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +conf@15.0.2: + version "15.0.2" + resolved "https://registry.yarnpkg.com/conf/-/conf-15.0.2.tgz#b983be81227a304b9f885fde6c86c7fe5902dc9d" + integrity sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw== + dependencies: + ajv "^8.17.1" + ajv-formats "^3.0.1" + atomically "^2.0.3" + debounce-fn "^6.0.0" + dot-prop "^10.0.0" + env-paths "^3.0.0" + json-schema-typed "^8.0.1" + semver "^7.7.2" + uint8array-extras "^1.5.0" + config-chain@^1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" @@ -1740,6 +1770,13 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" +debounce-fn@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/debounce-fn/-/debounce-fn-6.0.0.tgz#558169aed853eb3cf3a17c0a2438e1a91a7ba44f" + integrity sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ== + dependencies: + mimic-function "^5.0.0" + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -1885,6 +1922,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dot-prop@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-10.1.0.tgz#91dbeb6771a9d2c31eab11ade3fdb1d83c4376c4" + integrity sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q== + dependencies: + type-fest "^5.0.0" + dunder-proto@^1.0.0, dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" @@ -1940,6 +1984,11 @@ enhanced-resolve@^5.17.3: graceful-fs "^4.2.4" tapable "^2.2.0" +env-paths@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-3.0.0.tgz#2f1e89c2f6dbd3408e1b1711dd82d62e317f58da" + integrity sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A== + envinfo@^7.14.0: version "7.19.0" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.19.0.tgz#b4b4507a27e9900b0175f556167fd3a95f8623f1" @@ -3576,6 +3625,11 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== +json-schema-typed@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz#e98ee7b1899ff4a184534d1f167c288c66bbeff4" + integrity sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" @@ -3777,6 +3831,11 @@ mime-types@^2.1.27: dependencies: mime-db "1.44.0" +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -4112,11 +4171,6 @@ once@^1.3.0: dependencies: wrappy "1" -opencollective-postinstall@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" - integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== - optionator@^0.8.1: version "0.8.3" resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" @@ -4634,7 +4688,7 @@ semver@^7.3.5: dependencies: lru-cache "^6.0.0" -semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3: +semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@^7.7.2: version "7.7.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== @@ -5018,6 +5072,18 @@ strip-json-comments@^3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +stubborn-fs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/stubborn-fs/-/stubborn-fs-2.0.0.tgz#628750f81c51c44c04ef50fc70ed4d1caea4f1e9" + integrity sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA== + dependencies: + stubborn-utils "^1.0.1" + +stubborn-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stubborn-utils/-/stubborn-utils-1.0.2.tgz#0d9c58ab550f40936235056c7ea6febd925c4d41" + integrity sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg== + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" @@ -5066,6 +5132,11 @@ synckit@^0.9.1: "@pkgr/core" "^0.1.0" tslib "^2.6.2" +tagged-tag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tagged-tag/-/tagged-tag-1.0.0.tgz#a0b5917c2864cba54841495abfa3f6b13edcf4d6" + integrity sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng== + tapable@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz" @@ -5236,6 +5307,13 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^5.0.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.3.0.tgz#9422125b3094b1087d8446ba151b72fb9f39411a" + integrity sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g== + dependencies: + tagged-tag "^1.0.0" + typed-array-buffer@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" @@ -5293,6 +5371,11 @@ typescript@5.9.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== +uint8array-extras@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/uint8array-extras/-/uint8array-extras-1.5.0.tgz#10d2a85213de3ada304fea1c454f635c73839e86" + integrity sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A== + unbox-primitive@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" @@ -5449,6 +5532,11 @@ webpack@5.102.1: watchpack "^2.4.4" webpack-sources "^3.3.3" +when-exit@^2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/when-exit/-/when-exit-2.1.5.tgz#53fa4ffa2ba4c792213fb6617eb7d08f0dcb1a9f" + integrity sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg== + which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" From 6866396e9a74dd1d5d8c38f169a40889802907da Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Sat, 13 Dec 2025 18:43:52 +0400 Subject: [PATCH 02/43] Add `version` parameter to the `apiConfig` to use different versions of JavaScript Obfuscator Pro via API (#1353) --- CHANGELOG.md | 4 + README.md | 19 ++ package.json | 2 +- src/interfaces/pro-api/IProApiClient.ts | 6 + src/pro-api/ProApiClient.ts | 13 +- test/index.spec.ts | 1 + test/unit-tests/pro-api/ProApiClient.spec.ts | 254 +++++++++++++++++++ 7 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 test/unit-tests/pro-api/ProApiClient.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 359185db7..f0f9804bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.1.0 +--- +* Add `version` parameter to the `apiConfig` to use different versions JavaScript Obfuscator Pro via API + v5.0.1 --- * Add JavaScript Obfuscator PRO advertisement message diff --git a/README.md b/README.md index 5c2c4a38c..048be086f 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,7 @@ console.log(result.getObfuscatedCode()); * `apiConfig` (`Object`) – Pro API configuration: * `apiToken` (`string`, required) – your API token from obfuscator.io * `timeout` (`number`, optional) – request timeout in ms (default: `300000` - 5 minutes) + * `version` (`string`, optional) – JavaScript Obfuscator Pro version to use (e.g., `'5.0.0-beta.20'`). Defaults to latest version if not specified. * `onProgress` (`function`, optional) – callback for progress updates during obfuscation **Returns:** `Promise` @@ -317,6 +318,24 @@ console.log(result.getObfuscatedCode()); - API token is invalid or expired - API request fails +### Pro API with Specific Version + +You can specify which obfuscator version to use via the `version` option: + +```javascript +const result = await JavaScriptObfuscator.obfuscatePro( + sourceCode, + { + vmObfuscation: true, + vmObfuscationThreshold: 1 + }, + { + apiToken: 'your_javascript_obfuscator_pro_api_token', + version: '5.0.0-beta.20' // Use specific version + } +); +``` + ### Pro API with Progress Updates The API uses streaming mode to provide real-time progress updates during obfuscation: diff --git a/package.json b/package.json index 9e91a042e..ba8f9d0fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.0.1", + "version": "5.1.0", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", diff --git a/src/interfaces/pro-api/IProApiClient.ts b/src/interfaces/pro-api/IProApiClient.ts index 50b55ef69..46712c03e 100644 --- a/src/interfaces/pro-api/IProApiClient.ts +++ b/src/interfaces/pro-api/IProApiClient.ts @@ -40,6 +40,12 @@ export interface IProApiConfig { * Request timeout in milliseconds (default: 300000 - 5 minutes) */ timeout?: number; + + /** + * Obfuscator version to use (e.g., '5.0.0-beta.20') + * Defaults to latest version if not specified + */ + version?: string; } /** diff --git a/src/pro-api/ProApiClient.ts b/src/pro-api/ProApiClient.ts index 2951cda1d..54edd4d04 100644 --- a/src/pro-api/ProApiClient.ts +++ b/src/pro-api/ProApiClient.ts @@ -26,12 +26,14 @@ export class ProApiClient { private readonly config: { apiToken: string; timeout: number; + version?: string; }; public constructor(config: IProApiConfig) { this.config = { apiToken: config.apiToken, - timeout: config.timeout ?? DEFAULT_TIMEOUT + timeout: config.timeout ?? DEFAULT_TIMEOUT, + version: config.version }; } @@ -73,8 +75,15 @@ export class ProApiClient { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); + // Build URL with optional version parameter + let url = API_URL; + + if (this.config.version) { + url = `${API_URL}?version=${encodeURIComponent(this.config.version)}`; + } + try { - const response = await fetch(API_URL, { + const response = await fetch(url, { method: 'POST', headers, body, diff --git a/test/index.spec.ts b/test/index.spec.ts index f156f832e..72d2887ba 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -33,6 +33,7 @@ import './unit-tests/node/node-utils/NodeUtils.spec'; import './unit-tests/node/numerical-expression-data-to-node-converter/NumericalExpressionDataToNodeConverter.spec'; import './unit-tests/options/Options.spec'; import './unit-tests/options/ValidationErrorsFormatter.spec'; +import './unit-tests/pro-api/ProApiClient.spec'; import './unit-tests/source-code/ObfuscationResult.spec'; import './unit-tests/source-code/SourceCode.spec'; import './unit-tests/storages/ArrayStorage.spec'; diff --git a/test/unit-tests/pro-api/ProApiClient.spec.ts b/test/unit-tests/pro-api/ProApiClient.spec.ts new file mode 100644 index 000000000..470222ee7 --- /dev/null +++ b/test/unit-tests/pro-api/ProApiClient.spec.ts @@ -0,0 +1,254 @@ +import 'reflect-metadata'; + +import { assert } from 'chai'; +import * as sinon from 'sinon'; + +import { ProApiClient } from '../../../src/pro-api/ProApiClient'; +import { IProApiConfig } from '../../../src/interfaces/pro-api/IProApiClient'; +import { ApiError } from '../../../src/pro-api/ApiError'; + +describe('ProApiClient', () => { + const API_URL = 'https://obfuscator.io/api/v1/obfuscate'; + + let fetchStub: sinon.SinonStub; + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch'); + }); + + afterEach(() => { + fetchStub.restore(); + }); + + describe('constructor', () => { + describe('Variant #1: basic configuration', () => { + it('should create client with required apiToken', () => { + const config: IProApiConfig = { + apiToken: 'test-token' + }; + + const client = new ProApiClient(config); + + assert.isDefined(client); + }); + }); + + describe('Variant #2: configuration with all options', () => { + it('should create client with all configuration options', () => { + const config: IProApiConfig = { + apiToken: 'test-token', + timeout: 60000, + version: '5.0.0-beta.20' + }; + + const client = new ProApiClient(config); + + assert.isDefined(client); + }); + }); + }); + + describe('obfuscate', () => { + describe('Variant #1: vmObfuscation validation', () => { + it('should throw ApiError when vmObfuscation is not enabled', async () => { + const config: IProApiConfig = { + apiToken: 'test-token' + }; + const client = new ProApiClient(config); + + try { + await client.obfuscate('const a = 1;', { compact: true }); + assert.fail('Should have thrown ApiError'); + } catch (error) { + assert.instanceOf(error, ApiError); + assert.include((error as ApiError).message, 'vmObfuscation'); + } + }); + }); + + describe('Variant #2: URL without version parameter', () => { + it('should call API without version query parameter when version is not specified', async () => { + const config: IProApiConfig = { + apiToken: 'test-token' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'result', code: 'obfuscated', sourceMap: '' }), + { status: 200 } + ); + fetchStub.resolves(mockResponse); + + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.isTrue(fetchStub.calledOnce); + const calledUrl = fetchStub.firstCall.args[0]; + assert.strictEqual(calledUrl, API_URL); + }); + }); + + describe('Variant #3: URL with version parameter', () => { + it('should call API with version query parameter when version is specified', async () => { + const config: IProApiConfig = { + apiToken: 'test-token', + version: '5.0.0-beta.20' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'result', code: 'obfuscated', sourceMap: '' }), + { status: 200 } + ); + fetchStub.resolves(mockResponse); + + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.isTrue(fetchStub.calledOnce); + const calledUrl = fetchStub.firstCall.args[0]; + assert.strictEqual(calledUrl, `${API_URL}?version=5.0.0-beta.20`); + }); + }); + + describe('Variant #4: version parameter encoding', () => { + it('should properly encode version parameter in URL', async () => { + const config: IProApiConfig = { + apiToken: 'test-token', + version: '5.0.0-beta.22' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'result', code: 'obfuscated', sourceMap: '' }), + { status: 200 } + ); + fetchStub.resolves(mockResponse); + + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.isTrue(fetchStub.calledOnce); + const calledUrl = fetchStub.firstCall.args[0]; + // encodeURIComponent('5.0.0-beta.22') === '5.0.0-beta.22' (no special chars) + assert.strictEqual(calledUrl, `${API_URL}?version=5.0.0-beta.22`); + }); + }); + + describe('Variant #5: authorization header', () => { + it('should include Authorization header with Bearer token', async () => { + const config: IProApiConfig = { + apiToken: 'my-secret-token', + version: '5.0.0-beta.15' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'result', code: 'obfuscated', sourceMap: '' }), + { status: 200 } + ); + fetchStub.resolves(mockResponse); + + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.isTrue(fetchStub.calledOnce); + const calledOptions = fetchStub.firstCall.args[1]; + assert.strictEqual(calledOptions.headers['Authorization'], 'Bearer my-secret-token'); + }); + }); + + describe('Variant #6: successful obfuscation result', () => { + it('should return obfuscation result with code and sourceMap', async () => { + const config: IProApiConfig = { + apiToken: 'test-token', + version: '5.0.0-beta.20' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'result', code: 'var _0x123=1;', sourceMap: '{"version":3}' }), + { status: 200 } + ); + fetchStub.resolves(mockResponse); + + const result = await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.strictEqual(result.getObfuscatedCode(), 'var _0x123=1;'); + assert.strictEqual(result.getSourceMap(), '{"version":3}'); + }); + }); + + describe('Variant #7: chunked response', () => { + it('should reassemble chunked response correctly', async () => { + const config: IProApiConfig = { + apiToken: 'test-token', + version: '5.0.0-beta.10' + }; + const client = new ProApiClient(config); + + const chunks = [ + JSON.stringify({ type: 'progress', message: 'Processing...' }), + JSON.stringify({ type: 'chunk', field: 'code', data: 'var _0x', index: 0, total: 2 }), + JSON.stringify({ type: 'chunk', field: 'code', data: '123=1;', index: 1, total: 2 }), + JSON.stringify({ type: 'chunk_end', sourceMap: '' }) + ].join('\n'); + + const mockResponse = new Response(chunks, { status: 200 }); + fetchStub.resolves(mockResponse); + + const result = await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.strictEqual(result.getObfuscatedCode(), 'var _0x123=1;'); + }); + }); + + describe('Variant #8: API error response', () => { + it('should throw ApiError when API returns error message', async () => { + const config: IProApiConfig = { + apiToken: 'invalid-token', + version: '5.0.0-beta.20' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'error', message: 'Invalid API token' }), + { status: 401 } + ); + fetchStub.resolves(mockResponse); + + try { + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + assert.fail('Should have thrown ApiError'); + } catch (error) { + assert.instanceOf(error, ApiError); + assert.include((error as ApiError).message, 'Invalid API token'); + } + }); + }); + + describe('Variant #9: progress callback', () => { + it('should call progress callback for progress messages', async () => { + const config: IProApiConfig = { + apiToken: 'test-token', + version: '5.0.0-beta.20' + }; + const client = new ProApiClient(config); + + const progressMessages: string[] = []; + const onProgress = (message: string) => { + progressMessages.push(message); + }; + + const chunks = [ + JSON.stringify({ type: 'progress', message: 'Validating...' }), + JSON.stringify({ type: 'progress', message: 'Obfuscating...' }), + JSON.stringify({ type: 'result', code: 'var a=1;', sourceMap: '' }) + ].join('\n'); + + const mockResponse = new Response(chunks, { status: 200 }); + fetchStub.resolves(mockResponse); + + await client.obfuscate('const a = 1;', { vmObfuscation: true }, onProgress); + + assert.deepEqual(progressMessages, ['Validating...', 'Obfuscating...']); + }); + }); + }); +}); From 12aff3cfd491c54abe3672b1aa6344ac86c54bad Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Tue, 16 Dec 2025 23:02:58 +0400 Subject: [PATCH 03/43] Optimize performance (#1355) --- .../BlockStatementControlFlowTransformer.ts | 15 +- .../AbstractControlFlowReplacer.ts | 26 ++- .../ClassFieldTransformer.ts | 8 +- .../ObjectExpressionKeysTransformer.ts | 173 +++++++----------- .../SplitStringTransformer.ts | 23 ++- .../TemplateLiteralTransformer.ts | 41 +++-- .../BasePropertiesExtractor.ts | 4 +- .../DirectivePlacementTransformer.ts | 18 +- ...StringArrayScopeCallsWrapperTransformer.ts | 7 +- .../StringArrayTransformer.ts | 26 ++- .../StringArrayStorage.ts | 61 +++--- src/utils/ArrayUtils.ts | 21 +-- src/utils/EscapeSequenceEncoder.ts | 8 +- 13 files changed, 208 insertions(+), 223 deletions(-) diff --git a/src/node-transformers/control-flow-transformers/BlockStatementControlFlowTransformer.ts b/src/node-transformers/control-flow-transformers/BlockStatementControlFlowTransformer.ts index 54a9356cd..9a09ca261 100644 --- a/src/node-transformers/control-flow-transformers/BlockStatementControlFlowTransformer.ts +++ b/src/node-transformers/control-flow-transformers/BlockStatementControlFlowTransformer.ts @@ -77,6 +77,10 @@ export class BlockStatementControlFlowTransformer extends AbstractNodeTransforme * @returns {boolean} */ private static canTransformBlockStatementNode(blockStatementNode: ESTree.BlockStatement): boolean { + if (blockStatementNode.body.length <= 4) { + return false; + } + let canTransform: boolean = true; estraverse.traverse(blockStatementNode, { @@ -91,10 +95,6 @@ export class BlockStatementControlFlowTransformer extends AbstractNodeTransforme } }); - if (blockStatementNode.body.length <= 4) { - canTransform = false; - } - return canTransform; } @@ -138,8 +138,11 @@ export class BlockStatementControlFlowTransformer extends AbstractNodeTransforme const blockStatementBody: ESTree.Statement[] = blockStatementNode.body; const originalKeys: number[] = this.arrayUtils.createWithRange(blockStatementBody.length); const shuffledKeys: number[] = this.arrayUtils.shuffle(originalKeys); - const originalKeysIndexesInShuffledArray: number[] = originalKeys.map((key: number) => - shuffledKeys.indexOf(key) + const shuffledKeyToIndex: Map = new Map( + shuffledKeys.map((key: number, index: number) => [key, index]) + ); + const originalKeysIndexesInShuffledArray: number[] = originalKeys.map( + (key: number) => shuffledKeyToIndex.get(key)! ); const blockStatementControlFlowFlatteningCustomNode: ICustomNode< TInitialData diff --git a/src/node-transformers/control-flow-transformers/control-flow-replacers/AbstractControlFlowReplacer.ts b/src/node-transformers/control-flow-transformers/control-flow-replacers/AbstractControlFlowReplacer.ts index e26e2fee1..f19f20be2 100644 --- a/src/node-transformers/control-flow-transformers/control-flow-replacers/AbstractControlFlowReplacer.ts +++ b/src/node-transformers/control-flow-transformers/control-flow-replacers/AbstractControlFlowReplacer.ts @@ -68,10 +68,10 @@ export abstract class AbstractControlFlowReplacer implements IControlFlowReplace * @returns {string} */ public generateStorageKey(controlFlowStorage: IControlFlowStorage): string { - const key: string = this.randomGenerator.getRandomString(5); + let key: string = this.randomGenerator.getRandomString(5); - if (controlFlowStorage.has(key)) { - return this.generateStorageKey(controlFlowStorage); + while (controlFlowStorage.has(key)) { + key = this.randomGenerator.getRandomString(5); } return key; @@ -91,9 +91,21 @@ export abstract class AbstractControlFlowReplacer implements IControlFlowReplace usingExistingIdentifierChance: number ): string { const controlFlowStorageId: string = controlFlowStorage.getStorageId(); - const storageKeysById: Map = - this.replacerDataByControlFlowStorageId.get(controlFlowStorageId) ?? new Map(); - const storageKeysForCurrentId: string[] = storageKeysById.get(replacerId) ?? []; + + let storageKeysById: Map | undefined = + this.replacerDataByControlFlowStorageId.get(controlFlowStorageId); + + if (!storageKeysById) { + storageKeysById = new Map(); + this.replacerDataByControlFlowStorageId.set(controlFlowStorageId, storageKeysById); + } + + let storageKeysForCurrentId: string[] | undefined = storageKeysById.get(replacerId); + + if (!storageKeysForCurrentId) { + storageKeysForCurrentId = []; + storageKeysById.set(replacerId, storageKeysForCurrentId); + } const shouldPickFromStorageKeysById = this.randomGenerator.getMathRandom() < usingExistingIdentifierChance && storageKeysForCurrentId.length; @@ -105,8 +117,6 @@ export abstract class AbstractControlFlowReplacer implements IControlFlowReplace const storageKey: string = this.generateStorageKey(controlFlowStorage); storageKeysForCurrentId.push(storageKey); - storageKeysById.set(replacerId, storageKeysForCurrentId); - this.replacerDataByControlFlowStorageId.set(controlFlowStorageId, storageKeysById); controlFlowStorage.set(storageKey, customNode); return storageKey; diff --git a/src/node-transformers/converting-transformers/ClassFieldTransformer.ts b/src/node-transformers/converting-transformers/ClassFieldTransformer.ts index a8ef3ac58..f19ca4e96 100644 --- a/src/node-transformers/converting-transformers/ClassFieldTransformer.ts +++ b/src/node-transformers/converting-transformers/ClassFieldTransformer.ts @@ -31,9 +31,9 @@ import { NodeGuards } from '../../node/NodeGuards'; @injectable() export class ClassFieldTransformer extends AbstractNodeTransformer { /** - * @type {string[]} + * @type {string} */ - private static readonly ignoredNames: string[] = ['constructor']; + private static readonly ignoredName: string = 'constructor'; /** * @param {IRandomGenerator} randomGenerator @@ -98,7 +98,7 @@ export class ClassFieldTransformer extends AbstractNodeTransformer { classFieldNode: ESTree.MethodDefinition | ESTree.PropertyDefinition, keyNode: ESTree.Identifier ): ESTree.MethodDefinition | ESTree.PropertyDefinition { - if (!ClassFieldTransformer.ignoredNames.includes(keyNode.name) && !classFieldNode.computed) { + if (keyNode.name !== ClassFieldTransformer.ignoredName && !classFieldNode.computed) { classFieldNode.computed = true; classFieldNode.key = NodeFactory.literalNode(keyNode.name); } @@ -117,7 +117,7 @@ export class ClassFieldTransformer extends AbstractNodeTransformer { ): ESTree.MethodDefinition | ESTree.PropertyDefinition { if ( typeof keyNode.value === 'string' && - !ClassFieldTransformer.ignoredNames.includes(keyNode.value) && + keyNode.value !== ClassFieldTransformer.ignoredName && !classFieldNode.computed ) { classFieldNode.computed = true; diff --git a/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts b/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts index 15ac0ae2b..e0addf21f 100644 --- a/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts +++ b/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts @@ -54,136 +54,105 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer { } /** - * @param {ObjectExpression} objectExpressionNode - * @param {Node} objectExpressionParentNode - * @param {Statement} objectExpressionHostStatement - * @returns {boolean} + * Combined prohibition check result */ - private static isProhibitedObjectExpressionNode( - objectExpressionNode: ESTree.ObjectExpression, - objectExpressionParentNode: ESTree.Node, - objectExpressionHostStatement: ESTree.Statement - ): boolean { - return ( - ObjectExpressionKeysTransformer.isReferencedIdentifierName( - objectExpressionNode, - objectExpressionHostStatement - ) || - ObjectExpressionKeysTransformer.isProhibitedArrowFunctionExpression( - objectExpressionNode, - objectExpressionParentNode - ) || - ObjectExpressionKeysTransformer.isObjectExpressionWithCallExpression(objectExpressionNode) || - ObjectExpressionKeysTransformer.isProhibitedSequenceExpression( - objectExpressionNode, - objectExpressionHostStatement - ) - ); - } - - /** - * @param {Identifier | ThisExpression} node - * @returns {string} - */ - private static getReferencedIdentifierName(node: ESTree.Identifier | ESTree.ThisExpression): string { - if (NodeGuards.isIdentifierNode(node)) { - return node.name; - } else { - return ObjectExpressionKeysTransformer.thisIdentifierName; - } - } - - /** - * @param {ObjectExpression} objectExpressionNode - * @param {Node} objectExpressionHostNode - * @returns {boolean} - */ - private static isReferencedIdentifierName( + private static checkProhibitedPatterns( objectExpressionNode: ESTree.ObjectExpression, objectExpressionHostNode: ESTree.Node - ): boolean { + ): { hasReferencedIdentifier: boolean; hasCallExpression: boolean } { const identifierNamesSet: Set = new Set(); - let isReferencedIdentifierName: boolean = false; - let isCurrentNode: boolean = false; + let hasReferencedIdentifier: boolean = false; + let hasCallExpression: boolean = false; + let isInsideObjectExpression: boolean = false; - // should mark node as prohibited if identifier of node is referenced somewhere inside other nodes estraverse.traverse(objectExpressionHostNode, { + // eslint-disable-next-line complexity enter: (node: ESTree.Node): void | estraverse.VisitorOption => { if (node === objectExpressionNode) { - isCurrentNode = true; + isInsideObjectExpression = true; } - if (!NodeGuards.isIdentifierNode(node) && !NodeGuards.isThisExpressionNode(node)) { - return; + if (isInsideObjectExpression && !hasCallExpression) { + if (NodeGuards.isCallExpressionNode(node) || NodeGuards.isNewExpressionNode(node)) { + hasCallExpression = true; + } } - if (!isCurrentNode) { - identifierNamesSet.add(ObjectExpressionKeysTransformer.getReferencedIdentifierName(node)); + if (NodeGuards.isIdentifierNode(node) || NodeGuards.isThisExpressionNode(node)) { + const identifierName: string = NodeGuards.isIdentifierNode(node) + ? node.name + : ObjectExpressionKeysTransformer.thisIdentifierName; - return; + if (!isInsideObjectExpression) { + identifierNamesSet.add(identifierName); + } else if (identifierNamesSet.has(identifierName)) { + hasReferencedIdentifier = true; + } } - const hasReferencedIdentifierName: boolean = identifierNamesSet.has( - ObjectExpressionKeysTransformer.getReferencedIdentifierName(node) - ); - - if (hasReferencedIdentifierName) { - isReferencedIdentifierName = true; - + if (hasReferencedIdentifier && hasCallExpression) { return estraverse.VisitorOption.Break; } }, leave: (node: ESTree.Node): void | estraverse.VisitorOption => { if (node === objectExpressionNode) { - isCurrentNode = false; - - return estraverse.VisitorOption.Break; + isInsideObjectExpression = false; + if (hasReferencedIdentifier || hasCallExpression) { + return estraverse.VisitorOption.Break; + } } } }); - return isReferencedIdentifierName; + return { hasReferencedIdentifier, hasCallExpression }; } /** * @param {ObjectExpression} objectExpressionNode - * @param {Node} objectExpressionNodeParentNode + * @param {Node} objectExpressionParentNode + * @param {Statement} objectExpressionHostStatement * @returns {boolean} */ - private static isProhibitedArrowFunctionExpression( + private static isProhibitedObjectExpressionNode( objectExpressionNode: ESTree.ObjectExpression, - objectExpressionNodeParentNode: ESTree.Node + objectExpressionParentNode: ESTree.Node, + objectExpressionHostStatement: ESTree.Statement ): boolean { - return ( - NodeGuards.isArrowFunctionExpressionNode(objectExpressionNodeParentNode) && - objectExpressionNodeParentNode.body === objectExpressionNode + if ( + ObjectExpressionKeysTransformer.isProhibitedArrowFunctionExpression( + objectExpressionNode, + objectExpressionParentNode + ) || + ObjectExpressionKeysTransformer.isProhibitedSequenceExpression( + objectExpressionNode, + objectExpressionHostStatement + ) + ) { + return true; + } + + const { hasReferencedIdentifier, hasCallExpression } = ObjectExpressionKeysTransformer.checkProhibitedPatterns( + objectExpressionNode, + objectExpressionHostStatement ); + + return hasReferencedIdentifier || hasCallExpression; } /** * @param {ObjectExpression} objectExpressionNode + * @param {Node} objectExpressionNodeParentNode * @returns {boolean} */ - private static isObjectExpressionWithCallExpression(objectExpressionNode: ESTree.ObjectExpression): boolean { - let isCallExpressionLikeNodeFound: boolean = false; - - estraverse.traverse(objectExpressionNode, { - enter: (node: ESTree.Node): void | estraverse.VisitorOption => { - const isCallExpressionLikeNode = - NodeGuards.isCallExpressionNode(node) || NodeGuards.isNewExpressionNode(node); - - if (!isCallExpressionLikeNode) { - return; - } - - isCallExpressionLikeNodeFound = true; - - return estraverse.VisitorOption.Break; - } - }); - - return isCallExpressionLikeNodeFound; + private static isProhibitedArrowFunctionExpression( + objectExpressionNode: ESTree.ObjectExpression, + objectExpressionNodeParentNode: ESTree.Node + ): boolean { + return ( + NodeGuards.isArrowFunctionExpressionNode(objectExpressionNodeParentNode) && + objectExpressionNodeParentNode.body === objectExpressionNode + ); } /** @@ -263,32 +232,28 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer { return objectExpressionNode; } - return this.applyObjectExpressionKeysExtractorsRecursive( - ObjectExpressionKeysTransformer.objectExpressionExtractorNames, - objectExpressionNode, - hostStatement - ); + return this.applyObjectExpressionKeysExtractorsRecursive(objectExpressionNode, hostStatement, 0); } /** - * @param {ObjectExpressionExtractor[]} objectExpressionExtractorNames * @param {ObjectExpression} objectExpressionNode * @param {Statement} hostStatement + * @param {number} extractorIndex * @returns {Node} */ private applyObjectExpressionKeysExtractorsRecursive( - objectExpressionExtractorNames: ObjectExpressionExtractor[], objectExpressionNode: ESTree.ObjectExpression, - hostStatement: ESTree.Statement + hostStatement: ESTree.Statement, + extractorIndex: number ): ESTree.Node { - const newObjectExpressionExtractorNames: ObjectExpressionExtractor[] = [...objectExpressionExtractorNames]; - const objectExpressionExtractor: ObjectExpressionExtractor | undefined = - newObjectExpressionExtractorNames.shift(); + const objectExpressionExtractorNames = ObjectExpressionKeysTransformer.objectExpressionExtractorNames; - if (!objectExpressionExtractor) { + if (extractorIndex >= objectExpressionExtractorNames.length) { return objectExpressionNode; } + const objectExpressionExtractor: ObjectExpressionExtractor = objectExpressionExtractorNames[extractorIndex]; + const { nodeToReplace, objectExpressionHostStatement: newObjectExpressionHostStatement, @@ -299,9 +264,9 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer { ); this.applyObjectExpressionKeysExtractorsRecursive( - newObjectExpressionExtractorNames, newObjectExpressionNode, - newObjectExpressionHostStatement + newObjectExpressionHostStatement, + extractorIndex + 1 ); return nodeToReplace; diff --git a/src/node-transformers/converting-transformers/SplitStringTransformer.ts b/src/node-transformers/converting-transformers/SplitStringTransformer.ts index 2481cd35f..3ab4de010 100644 --- a/src/node-transformers/converting-transformers/SplitStringTransformer.ts +++ b/src/node-transformers/converting-transformers/SplitStringTransformer.ts @@ -152,26 +152,25 @@ export class SplitStringTransformer extends AbstractNodeTransformer { * @returns {BinaryExpression} */ private transformStringChunksToBinaryExpressionNode(chunks: string[]): ESTree.BinaryExpression { - const firstChunk: string | undefined = chunks.shift(); - const secondChunk: string | undefined = chunks.shift(); + const chunksLength: number = chunks.length; - if (!firstChunk || !secondChunk) { + if (chunksLength < 2) { throw new Error('First and second chunks values should not be empty'); } const initialBinaryExpressionNode: ESTree.BinaryExpression = NodeFactory.binaryExpressionNode( '+', - NodeFactory.literalNode(firstChunk), - NodeFactory.literalNode(secondChunk) + NodeFactory.literalNode(chunks[0]), + NodeFactory.literalNode(chunks[1]) ); - return chunks.reduce( - (binaryExpressionNode: ESTree.BinaryExpression, chunk: string) => { - const chunkLiteralNode: ESTree.Literal = NodeFactory.literalNode(chunk); + let result: ESTree.BinaryExpression = initialBinaryExpressionNode; - return NodeFactory.binaryExpressionNode('+', binaryExpressionNode, chunkLiteralNode); - }, - initialBinaryExpressionNode - ); + // Start from index 2 since we already used 0 and 1 + for (let i: number = 2; i < chunksLength; i++) { + result = NodeFactory.binaryExpressionNode('+', result, NodeFactory.literalNode(chunks[i])); + } + + return result; } } diff --git a/src/node-transformers/converting-transformers/TemplateLiteralTransformer.ts b/src/node-transformers/converting-transformers/TemplateLiteralTransformer.ts index ae684a594..9de8392a2 100644 --- a/src/node-transformers/converting-transformers/TemplateLiteralTransformer.ts +++ b/src/node-transformers/converting-transformers/TemplateLiteralTransformer.ts @@ -83,53 +83,60 @@ export class TemplateLiteralTransformer extends AbstractNodeTransformer { ): ESTree.Expression { const templateLiteralExpressions: ESTree.Expression[] = templateLiteralNode.expressions; - let nodes: ESTree.Expression[] = []; + const nodes: ESTree.Expression[] = []; + + const quasis: ESTree.TemplateElement[] = templateLiteralNode.quasis; + const quasisLength: number = quasis.length; + + for (let i: number = 0; i < quasisLength; i++) { + const templateElement: ESTree.TemplateElement = quasis[i]; - templateLiteralNode.quasis.forEach((templateElement: ESTree.TemplateElement) => { if (templateElement.value.cooked === undefined || templateElement.value.cooked === null) { - return; + continue; } nodes.push(NodeFactory.literalNode(templateElement.value.cooked)); - const expression: ESTree.Expression | undefined = templateLiteralExpressions.shift(); + const expression: ESTree.Expression | undefined = templateLiteralExpressions[i]; if (!expression) { - return; + continue; } nodes.push(expression); - }); + } - nodes = nodes.filter((node: ESTree.Literal | ESTree.Expression) => { + const filteredNodes: ESTree.Expression[] = nodes.filter((node: ESTree.Literal | ESTree.Expression) => { return !(NodeGuards.isLiteralNode(node) && node.value === ''); }); // since `+` is left-to-right associative // ensure the first node is a string if first/second isn't if ( - !TemplateLiteralTransformer.isLiteralNodeWithStringValue(nodes[0]) && - !TemplateLiteralTransformer.isLiteralNodeWithStringValue(nodes[1]) + !TemplateLiteralTransformer.isLiteralNodeWithStringValue(filteredNodes[0]) && + !TemplateLiteralTransformer.isLiteralNodeWithStringValue(filteredNodes[1]) ) { - nodes.unshift(NodeFactory.literalNode('')); + filteredNodes.unshift(NodeFactory.literalNode('')); } let transformedNode: ESTree.Node; - if (nodes.length > 1) { + if (filteredNodes.length > 1) { let root: ESTree.BinaryExpression = NodeFactory.binaryExpressionNode( '+', - nodes.shift(), - nodes.shift() + filteredNodes[0], + filteredNodes[1] ); - nodes.forEach((node: ESTree.Literal | ESTree.Expression) => { - root = NodeFactory.binaryExpressionNode('+', root, node); - }); + // Start from index 2 since we already used 0 and 1 + const filteredNodesLength: number = filteredNodes.length; + for (let i: number = 2; i < filteredNodesLength; i++) { + root = NodeFactory.binaryExpressionNode('+', root, filteredNodes[i]); + } transformedNode = root; } else { - transformedNode = nodes[0]; + transformedNode = filteredNodes[0]; } NodeUtils.parentizeAst(transformedNode); diff --git a/src/node-transformers/converting-transformers/object-expression-extractors/BasePropertiesExtractor.ts b/src/node-transformers/converting-transformers/object-expression-extractors/BasePropertiesExtractor.ts index 4eea6ae2e..b4c90440d 100644 --- a/src/node-transformers/converting-transformers/object-expression-extractors/BasePropertiesExtractor.ts +++ b/src/node-transformers/converting-transformers/object-expression-extractors/BasePropertiesExtractor.ts @@ -216,8 +216,10 @@ export class BasePropertiesExtractor implements IObjectExpressionExtractor { objectExpressionNode: ESTree.ObjectExpression, removablePropertyIds: number[] ): void { + const removablePropertyIdsSet: Set = new Set(removablePropertyIds); + objectExpressionNode.properties = objectExpressionNode.properties.filter( - (property: ESTree.Property | ESTree.SpreadElement, index: number) => !removablePropertyIds.includes(index) + (property: ESTree.Property | ESTree.SpreadElement, index: number) => !removablePropertyIdsSet.has(index) ); } } diff --git a/src/node-transformers/finalizing-transformers/DirectivePlacementTransformer.ts b/src/node-transformers/finalizing-transformers/DirectivePlacementTransformer.ts index 63f414774..8d25c0f93 100644 --- a/src/node-transformers/finalizing-transformers/DirectivePlacementTransformer.ts +++ b/src/node-transformers/finalizing-transformers/DirectivePlacementTransformer.ts @@ -127,21 +127,9 @@ export class DirectivePlacementTransformer extends AbstractNodeTransformer { // append new directive node at the top of lexical scope statements NodeAppender.prepend(nodeWithLexicalScopeStatements, [newDirectiveNode]); - // remove found directive node - let isDirectiveNodeRemoved: boolean = false; - estraverse.replace(nodeWithLexicalScopeStatements, { - enter: (node: ESTree.Node): estraverse.VisitorOption | undefined => { - if (isDirectiveNodeRemoved) { - return estraverse.VisitorOption.Break; - } - - if (node === directiveNode) { - isDirectiveNodeRemoved = true; - - return estraverse.VisitorOption.Remove; - } - } - }); + nodeWithLexicalScopeStatements.body = nodeWithLexicalScopeStatements.body.filter( + (node) => node !== directiveNode + ); } return nodeWithLexicalScopeStatements; diff --git a/src/node-transformers/string-array-transformers/StringArrayScopeCallsWrapperTransformer.ts b/src/node-transformers/string-array-transformers/StringArrayScopeCallsWrapperTransformer.ts index 68a7eb3ec..5aa19210a 100644 --- a/src/node-transformers/string-array-transformers/StringArrayScopeCallsWrapperTransformer.ts +++ b/src/node-transformers/string-array-transformers/StringArrayScopeCallsWrapperTransformer.ts @@ -140,15 +140,16 @@ export class StringArrayScopeCallsWrapperTransformer extends AbstractNodeTransfo const { scopeCallsWrappersData } = stringArrayScopeCallsWrappersData; const scopeCallsWrappersDataLength: number = scopeCallsWrappersData.length; + const upperStringArrayCallsWrapperData = this.getUpperStringArrayCallsWrapperData( + stringArrayScopeCallsWrappersData + ); + /** * Iterates over each name of scope wrapper name * Reverse iteration appends wrappers at index `0` at the correct order */ for (let i = scopeCallsWrappersDataLength - 1; i >= 0; i--) { const stringArrayScopeCallsWrapperData = scopeCallsWrappersData[i]; - const upperStringArrayCallsWrapperData = this.getUpperStringArrayCallsWrapperData( - stringArrayScopeCallsWrappersData - ); this.getAndAppendStringArrayScopeCallsWrapperNode( lexicalScopeBodyNode, diff --git a/src/node-transformers/string-array-transformers/StringArrayTransformer.ts b/src/node-transformers/string-array-transformers/StringArrayTransformer.ts index 6c80f5733..6993f1f9c 100644 --- a/src/node-transformers/string-array-transformers/StringArrayTransformer.ts +++ b/src/node-transformers/string-array-transformers/StringArrayTransformer.ts @@ -328,18 +328,24 @@ export class StringArrayTransformer extends AbstractNodeTransformer { const nextScopeCallsWrapperParameterIndexesData: IStringArrayScopeCallsWrapperParameterIndexesData | null = this.getStringArrayCallsWrapperParameterIndexesData(); - stringArrayScopeCallsWrappersDataByEncoding[encoding] = { - encoding, - scopeCallsWrappersData: [ - ...stringArrayScopeCallsWrappersData, - { - name: nextScopeCallsWrapperName, - index: nextScopeCallsWrapperShiftedIndex, - parameterIndexesData: nextScopeCallsWrapperParameterIndexesData - } - ] + const newWrapperData: IStringArrayScopeCallsWrapperData = { + name: nextScopeCallsWrapperName, + index: nextScopeCallsWrapperShiftedIndex, + parameterIndexesData: nextScopeCallsWrapperParameterIndexesData }; + let encodingData = stringArrayScopeCallsWrappersDataByEncoding[encoding]; + + if (!encodingData) { + encodingData = { + encoding, + scopeCallsWrappersData: [newWrapperData] + }; + stringArrayScopeCallsWrappersDataByEncoding[encoding] = encodingData; + } else { + encodingData.scopeCallsWrappersData.push(newWrapperData); + } + this.stringArrayScopeCallsWrappersDataStorage.set( currentLexicalScopeBodyNode, stringArrayScopeCallsWrappersDataByEncoding diff --git a/src/storages/string-array-transformers/StringArrayStorage.ts b/src/storages/string-array-transformers/StringArrayStorage.ts index ebdd04075..d58c8586d 100644 --- a/src/storages/string-array-transformers/StringArrayStorage.ts +++ b/src/storages/string-array-transformers/StringArrayStorage.ts @@ -78,9 +78,9 @@ export class StringArrayStorage private readonly rc4Keys: string[]; /** - * @type {Map} + * @type {Map>} */ - private readonly rc4EncodedValuesSourcesCache: Map = new Map(); + private readonly rc4EncodedValuesSourcesCache: Map> = new Map(); /** * @type {number} @@ -224,19 +224,13 @@ export class StringArrayStorage this.storage = new Map( this.arrayUtils .shuffle(Array.from(this.storage.entries())) - .map<[`${string}-${TStringArrayEncoding}`, IStringArrayStorageItemData]>( - ([value, stringArrayStorageItemData], index: number) => { - stringArrayStorageItemData.index = index; + .map< + [`${string}-${TStringArrayEncoding}`, IStringArrayStorageItemData] + >(([value, stringArrayStorageItemData], index: number) => { + stringArrayStorageItemData.index = index; - return [value, stringArrayStorageItemData]; - } - ) - .sort( - ( - [, stringArrayStorageItemDataA]: [string, IStringArrayStorageItemData], - [, stringArrayStorageItemDataB]: [string, IStringArrayStorageItemData] - ) => stringArrayStorageItemDataA.index - stringArrayStorageItemDataB.index - ) + return [value, stringArrayStorageItemData]; + }) ); } @@ -296,29 +290,36 @@ export class StringArrayStorage * if collision will happen, just try to encode value again */ case StringArrayEncoding.Rc4: { - const decodeKey: string = this.randomGenerator.getRandomGenerator().pickone(this.rc4Keys); - const encodedValue: string = this.cryptUtilsStringArray.btoa( - this.cryptUtilsStringArray.rc4(value, decodeKey) - ); + const maxRetryAttempts: number = 50; - const encodedValueSources: string[] = this.rc4EncodedValuesSourcesCache.get(encodedValue) ?? []; - let encodedValueSourcesLength: number = encodedValueSources.length; + for (let attempt: number = 0; attempt < maxRetryAttempts; attempt++) { + const decodeKey: string = this.randomGenerator.getRandomGenerator().pickone(this.rc4Keys); + const encodedValue: string = this.cryptUtilsStringArray.btoa( + this.cryptUtilsStringArray.rc4(value, decodeKey) + ); - const shouldAddValueToSourcesCache: boolean = - !encodedValueSourcesLength || !encodedValueSources.includes(value); + const encodedValueSources: Set = + this.rc4EncodedValuesSourcesCache.get(encodedValue) ?? new Set(); - if (shouldAddValueToSourcesCache) { - encodedValueSources.push(value); - encodedValueSourcesLength++; - } + const shouldAddValueToSourcesCache: boolean = + encodedValueSources.size === 0 || !encodedValueSources.has(value); + + if (shouldAddValueToSourcesCache) { + encodedValueSources.add(value); + } - this.rc4EncodedValuesSourcesCache.set(encodedValue, encodedValueSources); + this.rc4EncodedValuesSourcesCache.set(encodedValue, encodedValueSources); - if (encodedValueSourcesLength > 1) { - return this.getEncodedValue(value); + if (encodedValueSources.size <= 1) { + return { encodedValue, encoding, decodeKey }; + } } - return { encodedValue, encoding, decodeKey }; + return { + encodedValue: this.cryptUtilsStringArray.btoa(value), + encoding: StringArrayEncoding.Base64, + decodeKey: null + }; } case StringArrayEncoding.Base64: { diff --git a/src/utils/ArrayUtils.ts b/src/utils/ArrayUtils.ts index 718ee2298..4c1c25577 100644 --- a/src/utils/ArrayUtils.ts +++ b/src/utils/ArrayUtils.ts @@ -103,7 +103,9 @@ export class ArrayUtils implements IArrayUtils { * @returns {T[]} */ public rotate(array: T[], times: number): T[] { - if (!array.length) { + const arrayLength: number = array.length; + + if (!arrayLength) { throw new ReferenceError('Cannot rotate empty array.'); } @@ -111,19 +113,16 @@ export class ArrayUtils implements IArrayUtils { return array; } - const newArray: T[] = array; + // Normalize rotation amount to avoid unnecessary full rotations + // O(N) algorithm using slice instead of O(N*R) with pop/unshift + const normalizedTimes: number = times % arrayLength; - let temp: T | undefined; - - while (times--) { - temp = newArray.pop(); - - if (temp) { - newArray.unshift(temp); - } + if (normalizedTimes === 0) { + return [...array]; } - return newArray; + // Right rotation: take last `normalizedTimes` elements and put them at the front + return [...array.slice(-normalizedTimes), ...array.slice(0, -normalizedTimes)]; } /** diff --git a/src/utils/EscapeSequenceEncoder.ts b/src/utils/EscapeSequenceEncoder.ts index 6db012ab3..75511d7c3 100644 --- a/src/utils/EscapeSequenceEncoder.ts +++ b/src/utils/EscapeSequenceEncoder.ts @@ -19,6 +19,11 @@ export class EscapeSequenceEncoder implements IEscapeSequenceEncoder { */ private static readonly forceEscapeCharactersRegExp: RegExp = /[\x00-\x1F\x7F-\x9F'"\\\s]/; + /** + * @type {RegExp} + */ + private static readonly replaceRegExp: RegExp = /[\s\S]/g; + /** * @type {Map} */ @@ -37,12 +42,11 @@ export class EscapeSequenceEncoder implements IEscapeSequenceEncoder { } const radix: number = 16; - const replaceRegExp: RegExp = new RegExp('[\\s\\S]', 'g'); let prefix: string; let template: string; - const result: string = string.replace(replaceRegExp, (character: string): string => { + const result: string = string.replace(EscapeSequenceEncoder.replaceRegExp, (character: string): string => { const shouldEncodeCharacter: boolean = encodeAllSymbols || EscapeSequenceEncoder.forceEscapeCharactersRegExp.test(character); From 1c2d8bd9d096aa7fd8cf7b245dc82189df1eacce Mon Sep 17 00:00:00 2001 From: sanex3339 Date: Mon, 22 Dec 2025 09:29:21 +0400 Subject: [PATCH 04/43] Update readme --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 048be086f..b423e107e 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,20 @@ Huge thanks to all supporters! ![logo](https://raw.githubusercontent.com/javascript-obfuscator/javascript-obfuscator/master/images/logo.png) +--- + +### :rocket: JavaScript Obfuscator Pro with VM Obfuscation is out! + +**[JavaScript Obfuscator Pro](https://obfuscator.io/)** now features **VM-based bytecode obfuscation** β€” the most advanced code protection available. Your JavaScript functions are transformed into custom bytecode running on an embedded virtual machine, making reverse engineering extremely difficult. + +:star: **[Try it at obfuscator.io](https://obfuscator.io)** β€” user-friendly interface, cloud-based obfuscation, and Pro API access. + +--- + JavaScript Obfuscator is a powerful free obfuscator for JavaScript, containing a variety of features which provide protection for your source code. **Key features:** -- VM obfuscation (via [JavaScript Obfuscator Pro](https://obfuscator.io/)) +- VM bytecode obfuscation (via [JavaScript Obfuscator Pro](https://obfuscator.io/)) - variables renaming - strings extraction and encryption - dead code injection From a86314da5952154f63897d1f82ef2f151f89b825 Mon Sep 17 00:00:00 2001 From: sanex3339 Date: Mon, 22 Dec 2025 09:34:13 +0400 Subject: [PATCH 05/43] Update readme #2 --- README.md | 195 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 166 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index b423e107e..2fa197d00 100644 --- a/README.md +++ b/README.md @@ -1766,73 +1766,211 @@ The performance will be at a relatively normal level -## JavaScript Obfuscator Pro VM options +## JavaScript Obfuscator Pro Options + +> :warning: **The following VM obfuscation options are available only via the [JavaScript Obfuscator Pro API](https://obfuscator.io/).** +> +> To use these options, you need a Pro API token from [obfuscator.io](https://obfuscator.io) and must call the `obfuscatePro()` method instead of `obfuscate()`. See the [Pro API Methods](#shield-pro-api-methods-vm-obfuscation) section for details. ### `vmObfuscation` Type: `boolean` Default: `false` Enables VM-based bytecode obfuscation. When enabled, JavaScript functions are compiled into custom bytecode that runs on an embedded virtual machine. This provides the highest level of protection as the original code logic is completely transformed. -**Warning:** This significantly increases code size and may impact performance. Use `vmObfuscationThreshold` to control which root-level functions are transformed. +**Example:** +Your readable code like `return qty * price` becomes a list of numbers like `[0x15,0x03,0x17,...]` that only the embedded VM interpreter can execute. The original logic is no longer visible as JavaScript. ### `vmObfuscationThreshold` Type: `number` Default: `1` -The probability (from 0 to 1) that a function will be transformed to VM bytecode when `vmObfuscation` is enabled. - -- `0` - no functions will be transformed -- `0.5` - 50% of functions will be transformed -- `1` - all functions will be transformed +Controls what percentage of your root-level functions get VM protection. ### `vmTargetFunctions` Type: `string[]` Default: `[]` -Array of root-level function names to target for VM obfuscation. When specified, only these functions will be transformed (subject to `vmObfuscationThreshold`). Empty array means all functions are candidates. +Specify exactly which root-level functions should get VM protection by name. + +**Example:** +```javascript +{ + vmObfuscation: true, + vmTargetFunctions: ['someFunctionName'] +} +``` + +**Result:** Only these three functions get VM-protected. Everything else stays as regular (but still obfuscated) JavaScript. Perfect for protecting sensitive license checks or authentication logic while keeping the rest of your code lean. ### `vmExcludeFunctions` Type: `string[]` Default: `[]` -Array of root-level function names to exclude from VM obfuscation. These functions will never be transformed regardless of other settings. +Specify root-level functions that should never get VM protection. Takes precedence over other settings. + +**Example:** +```javascript +{ + vmObfuscation: true, + vmExcludeFunctions: ['someFunctionName'] +} +``` + +**When to use:** Performance-critical root-level functions (animation loops, real-time data processing) can be excluded to avoid VM overhead while still protecting everything else. + +### `vmTargetFunctionsMode` +Type: `string` Default: `root` + +Controls how functions/methods are selected for VM obfuscation. + +| Mode | Description | +|------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `root` | Default behavior. Only root-level functions are considered for VM obfuscation. Uses `vmTargetFunctions` allow-list and `vmExcludeFunctions` deny-list to filter. | +| `comment` | Only functions/methods decorated with `/* javascript-obfuscator:vm */` comment are VM-obfuscated. Works with functions/methods at **any nesting level**. | + +**Example - Comment mode:** +```javascript +// Source code +function regularFunction() { + return 'not virtualized'; +} + +/* javascript-obfuscator:vm */ +function sensitiveFunction() { + return 'this will be VM-protected'; +} + +function outer() { + /* javascript-obfuscator:vm */ + function nestedSensitive() { + return 'nested but still VM-protected'; + } + return nestedSensitive(); +} +``` + +```javascript +// Obfuscator options +{ + vmObfuscation: true, + vmTargetFunctionsMode: 'comment' +} +``` + +**When to use:** When you need surgical control over exactly which functions get VM protection, especially nested functions that contain sensitive logic. Unlike `vmTargetFunctions` which only works with root-level named functions, comment mode lets you protect any function anywhere in your code. + +### `vmWrapTopLevelInitializers` +Type: `boolean` Default: `false` + +Wraps some top-level variable initializers in IIFEs (Immediately Invoked Function Expressions) so they can be VM-obfuscated. + +**What it does:** +Without this option, top-level constants and variables remain visible in the output: +```javascript +// Input +const MY_STRING = "my-string"; + +// Output (without vmWrapTopLevelInitializers) +const MY_STRING = "my-string"; // String is visible! +``` + +With this option enabled, the initializer is wrapped in an IIFE that gets VM-obfuscated: +```javascript +// Input +const MY_STRING = "my-string"; + +// Output (with vmWrapTopLevelInitializers: true) +const MY_STRING = (() => { return /* VM bytecode call */ })(); // String hidden in bytecode +``` + +**Note:** This option only works when `vmTargetFunctionsMode` is `'root'` (the default). + +### `vmDynamicOpcodes` +Type: `boolean` Default: `false` + +Makes the VM interpreter smaller and unique for each build. + +**What it does:** +1. **Filters unused instructions** - If your code doesn't use classes, class-related instructions are removed entirely +2. **Randomizes structure** - The order of instruction handlers is shuffled each build + +As the result - smaller output and each build looks different. ### `vmOpcodeShuffle` Type: `boolean` Default: `false` -Randomizes the opcode mapping for each obfuscation run. Makes static analysis more difficult as opcode meanings change between builds. +Randomizes the numeric values assigned to each opcode. For example, the `LOAD` instruction might be `1` in one build and `47` in another. ### `vmBytecodeEncoding` Type: `boolean` Default: `false` -Encodes the bytecode instructions using XOR encryption. The decoding key is derived at runtime, adding another layer of protection. +Encodes each bytecode instruction. Instructions are decoded one at a time during execution. ### `vmBytecodeArrayEncoding` Type: `boolean` Default: `false` -Applies additional encoding to the bytecode array, making it harder to identify bytecode patterns through static analysis. +Encodes the entire bytecode array as a single block. The array is decoded once at startup before execution begins. Use together with `vmBytecodeEncoding` for two layers of protection. ### `vmJumpsEncoding` Type: `boolean` Default: `false` -Encodes jump targets and offsets in the bytecode. This obscures control flow and makes it harder to follow program execution. +Encodes jump targets in the bytecode. Jump offsets are calculated at runtime, hiding the control flow structure (`if`/`else`, loops, etc.) from static analysis. ### `vmDecoyOpcodes` Type: `boolean` Default: `false` -Inserts fake opcodes into the dispatcher that are never executed. Increases code complexity and confuses reverse engineering attempts. +Adds fake opcode handlers to the VM dispatcher that are never called. For example, if the VM uses 20 real opcodes, this might add 30 fake handlers, making the interpreter appear more complex than it really is. ### `vmDeadCodeInjection` Type: `boolean` Default: `false` -Injects dead code sequences into the VM bytecode. These sequences are valid but unreachable, adding noise to analysis. +Injects fake bytecode sequences that are never executed. These look like real instructions but are skipped during runtime, confusing analysis tools that process them. ### `vmSplitDispatcher` Type: `boolean` Default: `false` -Splits the VM dispatcher into multiple smaller dispatchers. Makes the execution flow harder to follow. +Splits the VM dispatcher into multiple smaller switch statements organized by opcode category, instead of one large monolithic switch. Each category (stack, arithmetic, control flow, etc.) gets its own switch, routed by if/else range checks. + +This option supports `vmDynamicOpcodes` in both modes: `true` (shuffle first, then split into groups) and `false`. + +> :warning: When `vmIndirectDispatch` is enabled, this option is ignored. Prefer `vmIndirectDispatch` as it provides better obfuscation with similar performance. + +### `vmIndirectDispatch` +Type: `boolean` Default: `false` + +Uses compile-time generated handler functions for opcode dispatch instead of switch statements. Handlers are generated at compile-time with inlined opcode logic and shuffled positions. + +Instead of: +```javascript +switch(op) { + case 0: /* handle opcode 0 */ break; + case 1: /* handle opcode 1 */ break; +} +``` + +It generates: +```javascript +var _hm = {0:42, 1:17, ...}; // opcode β†’ handler index mapping +var _h = [handler0, handler1, ...]; // shuffled handler array +_h[_hm[op]](arg); // single lookup + function call +``` + +This option supports `vmDynamicOpcodes` in both modes. + +> :warning: When enabled, this takes priority over `vmSplitDispatcher`. Both options cannot be active simultaneously. + +### `vmCompactDispatcher` +Type: `boolean` Default: `false` + +Uses a single unified dispatcher (generator-based) for both sync and async/generator code execution. By default (`false`), the VM generates two separate dispatchers: a non-generator version for sync code (faster) and a generator version for async/generator code. When enabled, only the generator-based dispatcher is used for all execution. + +**Trade-offs:** +- `false` (default): Larger code size due to dual dispatchers, but faster sync execution (no generator overhead) +- `true`: Smaller code size with single dispatcher, but sync code has generator protocol overhead + +Use this when code size is more important than sync execution speed. ### `vmMacroOps` Type: `boolean` Default: `false` -Combines common instruction sequences into single macro opcodes. This creates unique instruction patterns that are harder to recognize. +Combines common instruction sequences into single "macro" opcodes. For example, `LOAD + ADD + STORE` might become a single `MACRO_ADD_TO_VAR` instruction. This breaks pattern recognition and can improve performance. ### `vmDebugProtection` Type: `boolean` Default: `false` @@ -1842,34 +1980,33 @@ Adds anti-debugging measures to the VM runtime. Detects debugger presence and al ### `vmRuntimeOpcodeDerivation` Type: `boolean` Default: `false` -Derives opcode values at runtime through mathematical operations rather than using static values. Makes static analysis significantly harder. +Derives the opcode mapping table at runtime from a seed value instead of hardcoding it. The seed is stored in the bytecode and used to generate the opcode-to-handler mapping via Fisher-Yates shuffle during execution. ### `vmStatefulOpcodes` Type: `boolean` Default: `false` -Makes opcode interpretation depend on VM state. The same opcode can have different meanings based on execution history. +Makes opcode meanings depend on position in the bytecode. Each position has a different opcode-to-handler mapping derived from a seed, so the same opcode number performs different operations at different positions. ### `vmStackEncoding` Type: `boolean` Default: `false` -Encodes values pushed to and popped from the VM stack. Adds protection against memory inspection during execution. +Encrypts values on the VM stack during execution. Values are encoded when pushed and decoded when popped, so memory inspection shows encrypted data instead of actual values. -### `vmRandomizeKeys` -Type: `boolean` Default: `false` +This option heavily affects performance. -Randomizes encryption keys and other constants used by the VM. Each build produces unique key values. - -### `vmIndirectDispatch` +### `vmRandomizeKeys` Type: `boolean` Default: `false` -Uses indirect function calls for opcode dispatch instead of direct switch/case. Makes control flow analysis more difficult. +Randomizes the property key names used in bytecode objects. Standard keys like `i` (instructions), `c` (constants) become random 2-character identifiers, making the bytecode structure different for each build. ### `vmBytecodeFormat` Type: `string` Default: `binary` -Specifies the format used to embed bytecode in the output: -- `binary` - Compact binary representation (smaller size) -- `json` - JSON format (easier debugging, larger size) +Controls how bytecode is stored in the output. + +**Options:** +- `binary` - Compact binary format. Smaller size, recommended for production. +- `json` - Human-readable JSON format. Larger size, useful for debugging. ## Frequently Asked Questions From 90362a255593be2cd7668cb09e6007732dce569f Mon Sep 17 00:00:00 2001 From: sanex3339 Date: Mon, 22 Dec 2025 09:49:51 +0400 Subject: [PATCH 06/43] Update readme #3 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2fa197d00..31138b95c 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ Huge thanks to all supporters! ### :rocket: JavaScript Obfuscator Pro with VM Obfuscation is out! -**[JavaScript Obfuscator Pro](https://obfuscator.io/)** now features **VM-based bytecode obfuscation** β€” the most advanced code protection available. Your JavaScript functions are transformed into custom bytecode running on an embedded virtual machine, making reverse engineering extremely difficult. +**JavaScript Obfuscator Pro** features **VM-based bytecode obfuscation** β€” the most advanced code protection available. Your JavaScript functions are transformed into custom bytecode running on an embedded virtual machine, making reverse engineering extremely difficult. -:star: **[Try it at obfuscator.io](https://obfuscator.io)** β€” user-friendly interface, cloud-based obfuscation, and Pro API access. +[Try it at obfuscator.io](https://obfuscator.io) --- From c844f97cfc8200447d49540b5ff127f11c87a589 Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Tue, 6 Jan 2026 23:37:42 +0400 Subject: [PATCH 07/43] Ignore transformation of `process.env.*` (#1367) --- .gitignore | 1 + .npmignore | 1 + CHANGELOG.md | 4 ++ CLAUDE.md | 4 +- CODE_OF_CONDUCT.md | 2 +- README.md | 4 +- package.json | 9 +-- .../PreparingTransformersModule.ts | 6 ++ .../obfuscating-guards/ObfuscatingGuard.ts | 1 + .../ObfuscatingGuardsTransformer.ts | 1 + .../ProcessEnvObfuscationGuard.ts | 66 +++++++++++++++++++ .../JavaScriptObfuscator.spec.ts | 18 +++++ .../fixtures/process-env.js | 1 + webpack/utils/WebpackUtils.js | 2 +- webpack/webpack.browser.config.js | 13 ++++ webpack/webpack.node.config.js | 13 ++++ 16 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 src/node-transformers/preparing-transformers/obfuscating-guards/ProcessEnvObfuscationGuard.ts create mode 100644 test/functional-tests/javascript-obfuscator/fixtures/process-env.js diff --git a/.gitignore b/.gitignore index cfe4fd231..ac8242982 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ npm-debug.log /test/benchmark/**/** *dockerfile /test*.js +/reproductions diff --git a/.npmignore b/.npmignore index e7fb7148d..2157c5a98 100644 --- a/.npmignore +++ b/.npmignore @@ -12,3 +12,4 @@ /test*.js index.ts index.cli.ts +/reproductions \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f0f9804bf..013df5696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.2.0 +--- +* Skip obfuscation of `process.env.*` + v5.1.0 --- * Add `version` parameter to the `apiConfig` to use different versions JavaScript Obfuscator Pro via API diff --git a/CLAUDE.md b/CLAUDE.md index 3236bb57e..03d0777fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ **JavaScript Obfuscator** is a powerful, enterprise-grade code obfuscation tool for JavaScript and Node.js applications. It transforms readable JavaScript code into a protected, difficult-to-understand format while maintaining full functionality. The project is widely used for protecting intellectual property and preventing reverse engineering. - **Version**: 5.0.0 -- **Author**: Timofey Kachalov (@sanex3339) +- **Author**: Timofei Kachalov (@sanex3339) - **License**: BSD-2-Clause - **Repository**: https://github.com/javascript-obfuscator/javascript-obfuscator - **Homepage**: https://obfuscator.io/ @@ -1429,7 +1429,7 @@ Use [grunt-contrib-obfuscator](https://github.com/javascript-obfuscator/grunt-co **BSD-2-Clause License** -Copyright (C) 2016-2024 Timofey Kachalov +Copyright (C) 2016-2026 Timofei Kachalov See `LICENSE.BSD` for full license text. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c2a51b853..14aa477fa 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sanex3339@yandex.ru. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@obfuscator.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/README.md b/README.md index 31138b95c..8bd9ebe6c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ #### You can support this project by donating: @@ -2101,7 +2101,7 @@ Become a sponsor and get your logo on our README on Github with a link to your s ## License [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjavascript-obfuscator%2Fjavascript-obfuscator.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjavascript-obfuscator%2Fjavascript-obfuscator?ref=badge_large) -Copyright (C) 2016-2024 [Timofey Kachalov](http://github.com/sanex3339). +Copyright (C) 2016-2026 [Timofei Kachalov](http://github.com/sanex3339). Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/package.json b/package.json index ba8f9d0fe..7bb02fa54 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.1.0", + "version": "5.2.0", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", @@ -129,12 +129,9 @@ "prepare": "husky install" }, "author": { - "name": "Timofey Kachalov" + "name": "Timofei Kachalov" }, - "contributors": [ - "Timofey Kachalov (https://github.com/sanex3339)", - "Dmitry Zamotkin (https://github.com/zamotkin)" - ], + "contributors": ["Timofei Kachalov (https://github.com/sanex3339)", "Dmitry Zamotkin (https://github.com/zamotkin)"], "license": "BSD-2-Clause", "packageManager": "yarn@1.22.21+sha512.ca75da26c00327d26267ce33536e5790f18ebd53266796fbb664d2a4a5116308042dd8ee7003b276a20eace7d3c5561c3577bdd71bcb67071187af124779620a" } diff --git a/src/container/modules/node-transformers/PreparingTransformersModule.ts b/src/container/modules/node-transformers/PreparingTransformersModule.ts index da19c7a6a..296b632f3 100644 --- a/src/container/modules/node-transformers/PreparingTransformersModule.ts +++ b/src/container/modules/node-transformers/PreparingTransformersModule.ts @@ -15,6 +15,7 @@ import { EvalCallExpressionTransformer } from '../../../node-transformers/prepar import { ForceTransformStringObfuscatingGuard } from '../../../node-transformers/preparing-transformers/obfuscating-guards/ForceTransformStringObfuscatingGuard'; import { IgnoredImportObfuscatingGuard } from '../../../node-transformers/preparing-transformers/obfuscating-guards/IgnoredImportObfuscatingGuard'; import { ImportMetaObfuscationGuard } from '../../../node-transformers/preparing-transformers/obfuscating-guards/ImportMetaObfuscationGuard'; +import { ProcessEnvObfuscationGuard } from '../../../node-transformers/preparing-transformers/obfuscating-guards/ProcessEnvObfuscationGuard'; import { MetadataTransformer } from '../../../node-transformers/preparing-transformers/MetadataTransformer'; import { ObfuscatingGuardsTransformer } from '../../../node-transformers/preparing-transformers/ObfuscatingGuardsTransformer'; import { ParentificationTransformer } from '../../../node-transformers/preparing-transformers/ParentificationTransformer'; @@ -73,6 +74,11 @@ export const preparingTransformersModule: interfaces.ContainerModule = new Conta .inSingletonScope() .whenTargetNamed(ObfuscatingGuard.ImportMetaObfuscationGuard); + bind(ServiceIdentifiers.INodeGuard) + .to(ProcessEnvObfuscationGuard) + .inSingletonScope() + .whenTargetNamed(ObfuscatingGuard.ProcessEnvObfuscationGuard); + bind(ServiceIdentifiers.INodeGuard) .to(ReservedStringObfuscatingGuard) .inSingletonScope() diff --git a/src/enums/node-transformers/preparing-transformers/obfuscating-guards/ObfuscatingGuard.ts b/src/enums/node-transformers/preparing-transformers/obfuscating-guards/ObfuscatingGuard.ts index 3da243455..50dd66c8b 100644 --- a/src/enums/node-transformers/preparing-transformers/obfuscating-guards/ObfuscatingGuard.ts +++ b/src/enums/node-transformers/preparing-transformers/obfuscating-guards/ObfuscatingGuard.ts @@ -4,5 +4,6 @@ export enum ObfuscatingGuard { ForceTransformStringObfuscatingGuard = 'ForceTransformStringObfuscatingGuard', IgnoredImportObfuscatingGuard = 'IgnoredImportObfuscatingGuard', ImportMetaObfuscationGuard = 'ImportMetaObfuscationGuard', + ProcessEnvObfuscationGuard = 'ProcessEnvObfuscationGuard', ReservedStringObfuscatingGuard = 'ReservedStringObfuscatingGuard' } diff --git a/src/node-transformers/preparing-transformers/ObfuscatingGuardsTransformer.ts b/src/node-transformers/preparing-transformers/ObfuscatingGuardsTransformer.ts index c05f019b2..8d10290c8 100644 --- a/src/node-transformers/preparing-transformers/ObfuscatingGuardsTransformer.ts +++ b/src/node-transformers/preparing-transformers/ObfuscatingGuardsTransformer.ts @@ -33,6 +33,7 @@ export class ObfuscatingGuardsTransformer extends AbstractNodeTransformer { ObfuscatingGuard.ForceTransformStringObfuscatingGuard, ObfuscatingGuard.IgnoredImportObfuscatingGuard, ObfuscatingGuard.ImportMetaObfuscationGuard, + ObfuscatingGuard.ProcessEnvObfuscationGuard, ObfuscatingGuard.ReservedStringObfuscatingGuard ]; diff --git a/src/node-transformers/preparing-transformers/obfuscating-guards/ProcessEnvObfuscationGuard.ts b/src/node-transformers/preparing-transformers/obfuscating-guards/ProcessEnvObfuscationGuard.ts new file mode 100644 index 000000000..efbd23e1d --- /dev/null +++ b/src/node-transformers/preparing-transformers/obfuscating-guards/ProcessEnvObfuscationGuard.ts @@ -0,0 +1,66 @@ +import { injectable } from 'inversify'; + +import * as ESTree from 'estree'; + +import { IObfuscatingGuard } from '../../../interfaces/node-transformers/preparing-transformers/obfuscating-guards/IObfuscatingGuard'; + +import { ObfuscatingGuardResult } from '../../../enums/node/ObfuscatingGuardResult'; + +import { NodeGuards } from '../../../node/NodeGuards'; + +@injectable() +export class ProcessEnvObfuscationGuard implements IObfuscatingGuard { + /** + * @param {Node} node + * @return {boolean} + * @private + */ + private static isProcessEnvMemberExpression(node: ESTree.Node): boolean { + if (!NodeGuards.isMemberExpressionNode(node)) { + return false; + } + + return ( + NodeGuards.isIdentifierNode(node.object) && + node.object.name === 'process' && + NodeGuards.isIdentifierNode(node.property) && + node.property.name === 'env' && + !node.computed + ); + } + + /** + * @param {Node} node + * @return {boolean} + * @private + */ + private static isPartOfProcessEnvChain(node: ESTree.Node): boolean { + if (ProcessEnvObfuscationGuard.isProcessEnvMemberExpression(node)) { + return true; + } + + const parentNode = node.parentNode; + + if (parentNode && NodeGuards.isMemberExpressionNode(parentNode)) { + if (ProcessEnvObfuscationGuard.isProcessEnvMemberExpression(parentNode.object)) { + return true; + } + + if (ProcessEnvObfuscationGuard.isProcessEnvMemberExpression(parentNode)) { + return true; + } + } + + return false; + } + + /** + * @param {Node} node + * @returns {ObfuscatingGuardResult} + */ + public check(node: ESTree.Node): ObfuscatingGuardResult { + return ProcessEnvObfuscationGuard.isPartOfProcessEnvChain(node) + ? ObfuscatingGuardResult.Ignore + : ObfuscatingGuardResult.Transform; + } +} diff --git a/test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts b/test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts index 6a12c7ab2..4955b44d8 100644 --- a/test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts +++ b/test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts @@ -721,6 +721,24 @@ describe('JavaScriptObfuscator', () => { }); }); + describe('process.env.* support', () => { + const regExp: RegExp = /console\['log']\(process\.env\.FOO\);/; + + let obfuscatedCode: string; + + beforeEach(() => { + const code: string = readFileAsString(__dirname + '/fixtures/process-env.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should not obfuscate `process.env.*`', () => { + assert.match(obfuscatedCode, regExp); + }); + }); + /** * https://github.com/javascript-obfuscator/javascript-obfuscator/issues/710 */ diff --git a/test/functional-tests/javascript-obfuscator/fixtures/process-env.js b/test/functional-tests/javascript-obfuscator/fixtures/process-env.js new file mode 100644 index 000000000..d375f0d17 --- /dev/null +++ b/test/functional-tests/javascript-obfuscator/fixtures/process-env.js @@ -0,0 +1 @@ +console.log(process.env.FOO); diff --git a/webpack/utils/WebpackUtils.js b/webpack/utils/WebpackUtils.js index 572e41051..811923509 100644 --- a/webpack/utils/WebpackUtils.js +++ b/webpack/utils/WebpackUtils.js @@ -1,6 +1,6 @@ const fs = require('fs'); -const copyright = 'Copyright (C) 2016-2024 Timofey Kachalov '; +const copyright = 'Copyright (C) 2016-2026 Timofei Kachalov '; const sourceMapSupportRequire = 'require("source-map-support").install();'; class WebpackUtils { diff --git a/webpack/webpack.browser.config.js b/webpack/webpack.browser.config.js index 1a7ed3fb1..f6df3bfb6 100644 --- a/webpack/webpack.browser.config.js +++ b/webpack/webpack.browser.config.js @@ -1,6 +1,7 @@ 'use strict'; const webpack = require('webpack'); +const TerserPlugin = require('terser-webpack-plugin'); const packageJson = require('pjson'); const WebpackUtils = require('./utils/WebpackUtils').WebpackUtils; @@ -41,6 +42,18 @@ module.exports = { process: ['process'] }) ], + optimization: { + minimizer: [ + new TerserPlugin({ + extractComments: false, + terserOptions: { + format: { + comments: /^!/ // Keep comments starting with ! + } + } + }) + ] + }, output: { libraryTarget: 'umd', library: 'JavaScriptObfuscator', diff --git a/webpack/webpack.node.config.js b/webpack/webpack.node.config.js index 5761bad6b..3da2d2da5 100644 --- a/webpack/webpack.node.config.js +++ b/webpack/webpack.node.config.js @@ -4,6 +4,7 @@ const path = require('path'); const nodeExternals = require('webpack-node-externals'); const webpack = require('webpack'); +const TerserPlugin = require('terser-webpack-plugin'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin'); const ESLintPlugin = require('eslint-webpack-plugin'); @@ -70,6 +71,18 @@ module.exports = { skipFirstNotification: true }) ], + optimization: { + minimizer: [ + new TerserPlugin({ + extractComments: false, + terserOptions: { + format: { + comments: /^!/ // Keep comments starting with ! + } + } + }) + ] + }, output: { libraryTarget: 'commonjs2' }, From 311799e3e7c8a0c057d54808b6e700a583531a68 Mon Sep 17 00:00:00 2001 From: sanex3339 Date: Sun, 25 Jan 2026 23:21:42 +0400 Subject: [PATCH 08/43] Add vmPreprocessIdentifiers to the readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 8bd9ebe6c..93afd65d1 100644 --- a/README.md +++ b/README.md @@ -1785,6 +1785,13 @@ Type: `number` Default: `1` Controls what percentage of your root-level functions get VM protection. +### `vmPreprocessIdentifiers` +Type: `boolean` Default: `true` + +Renames all non-global identifiers to unique hexadecimal names before VM obfuscation. This eliminates variable shadowing that can cause scope resolution issues in the VM bytecode. + +**When to disable:** Only disable this if you encounter specific compatibility issues. The preprocessing step ensures correct variable resolution in complex nested scopes. + ### `vmTargetFunctions` Type: `string[]` Default: `[]` From 2be70353c734cc423353bdb48c22e44f16b97fe1 Mon Sep 17 00:00:00 2001 From: sanex3339 Date: Sun, 25 Jan 2026 23:22:53 +0400 Subject: [PATCH 09/43] Add strictMode to the readme --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 93afd65d1..128eaba72 100644 --- a/README.md +++ b/README.md @@ -1768,7 +1768,7 @@ The performance will be at a relatively normal level ## JavaScript Obfuscator Pro Options -> :warning: **The following VM obfuscation options are available only via the [JavaScript Obfuscator Pro API](https://obfuscator.io/).** +> :warning: **The following VM obfuscation/Pro options are available only via the [JavaScript Obfuscator Pro API](https://obfuscator.io/).** > > To use these options, you need a Pro API token from [obfuscator.io](https://obfuscator.io) and must call the `obfuscatePro()` method instead of `obfuscate()`. See the [Pro API Methods](#shield-pro-api-methods-vm-obfuscation) section for details. @@ -2015,6 +2015,16 @@ Controls how bytecode is stored in the output. - `binary` - Compact binary format. Smaller size, recommended for production. - `json` - Human-readable JSON format. Larger size, useful for debugging. +### `strictMode` +Type: `boolean | null` Default: `null` + +Allows to specify how the obfuscator should treat code regarding JavaScript strict mode. + +Available values: +* `null` (default) - auto-detect strict mode from the code. If the code has explicit `'use strict'` directive, ES module syntax, or class methods, it's treated as strict mode. Otherwise, sloppy mode is assumed. +* `true` - force strict mode treatment for all code, even without explicit `'use strict'` directive. Use this when your code will run in strict mode context (e.g., in ES modules, bundlers, or modern frameworks). +* `false` - only explicit strict mode indicators (`'use strict'`, ES modules, class methods) are treated as strict. Parent scope inheritance still applies per JS spec. + ## Frequently Asked Questions ### What javascript versions are supported? From 99c635a6d2f87541e03ce4ef5dfa0026c050aba6 Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Tue, 27 Jan 2026 14:04:39 +0400 Subject: [PATCH 10/43] Fixed `controlFlowFlattening` breaking short-circuit evaluation with spread operator and conditional objects (#1373) --- CHANGELOG.md | 1 + .../LogicalExpressionControlFlowReplacer.ts | 31 +++++++ .../issues/fixtures/issue1372.js | 15 ++++ .../functional-tests/issues/issue1372.spec.ts | 90 +++++++++++++++++++ ...ConditionalCommentObfuscatingGuard.spec.ts | 6 +- 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 test/functional-tests/issues/fixtures/issue1372.js create mode 100644 test/functional-tests/issues/issue1372.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 013df5696..c289cddab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Change Log v5.2.0 --- * Skip obfuscation of `process.env.*` +* Fixed `controlFlowFlattening` breaking short-circuit evaluation with spread operator and conditional objects. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1372 v5.1.0 --- diff --git a/src/node-transformers/control-flow-transformers/control-flow-replacers/LogicalExpressionControlFlowReplacer.ts b/src/node-transformers/control-flow-transformers/control-flow-replacers/LogicalExpressionControlFlowReplacer.ts index d77f1c60d..d4f5d6265 100644 --- a/src/node-transformers/control-flow-transformers/control-flow-replacers/LogicalExpressionControlFlowReplacer.ts +++ b/src/node-transformers/control-flow-transformers/control-flow-replacers/LogicalExpressionControlFlowReplacer.ts @@ -2,6 +2,7 @@ import { inject, injectable } from 'inversify'; import { ServiceIdentifiers } from '../../../container/ServiceIdentifiers'; import * as ESTree from 'estree'; +import * as estraverse from '@javascript-obfuscator/estraverse'; import { TControlFlowCustomNodeFactory } from '../../../types/container/custom-nodes/TControlFlowCustomNodeFactory'; import { TIdentifierNamesGeneratorFactory } from '../../../types/container/generators/TIdentifierNamesGeneratorFactory'; @@ -88,6 +89,10 @@ export class LogicalExpressionControlFlowReplacer extends ExpressionWithOperator leftExpression: ESTree.Expression, rightExpression: ESTree.Expression ): boolean { + if (this.expressionContainsProhibitedNodes(rightExpression)) { + return true; + } + return [leftExpression, rightExpression].some((expressionNode: ESTree.Node | ESTree.Expression): boolean => { let nodeForCheck: ESTree.Node | ESTree.Expression; @@ -105,4 +110,30 @@ export class LogicalExpressionControlFlowReplacer extends ExpressionWithOperator ); }); } + + /** + * @param {Expression} expression + * @returns {boolean} + */ + private expressionContainsProhibitedNodes(expression: ESTree.Expression): boolean { + let hasProhibitedNode = false; + + estraverse.traverse(expression, { + enter: (node: ESTree.Node): estraverse.VisitorOption | void => { + if (NodeGuards.isMemberExpressionNode(node) && node.computed) { + hasProhibitedNode = true; + + return estraverse.VisitorOption.Break; + } + + if (NodeGuards.isCallExpressionNode(node)) { + hasProhibitedNode = true; + + return estraverse.VisitorOption.Break; + } + } + }); + + return hasProhibitedNode; + } } diff --git a/test/functional-tests/issues/fixtures/issue1372.js b/test/functional-tests/issues/fixtures/issue1372.js new file mode 100644 index 000000000..cc1dfb95f --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1372.js @@ -0,0 +1,15 @@ +function test(hasColor) +{ + const accent = hasColor ? "primary" : undefined; + const theme = { palette: { primary: { main: "ok" } }}; + + const obj = + { + bgcolor: "test", + ...(accent && { color: theme.palette[accent].main }) + } + + return obj; +} + +module.exports = { test }; diff --git a/test/functional-tests/issues/issue1372.spec.ts b/test/functional-tests/issues/issue1372.spec.ts new file mode 100644 index 000000000..9b5da5f38 --- /dev/null +++ b/test/functional-tests/issues/issue1372.spec.ts @@ -0,0 +1,90 @@ +import { assert } from 'chai'; +import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes'; +import { readFileAsString } from '../../helpers/readFileAsString'; +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +// +// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1372 +// +describe('Issue #1372', () => { + describe('Spread operator with conditional object should work correctly', () => { + const samplesCount = 50; + + let obfuscatedCode: string; + + it('does not break on run with controlFlowFlattening enabled', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1372.js'); + + for (let i = 0; i < samplesCount; i++) { + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + controlFlowFlattening: true, + controlFlowFlatteningThreshold: 1, + stringArray: true, + stringArrayThreshold: 1 + }).getObfuscatedCode(); + + const result = eval(` + ${obfuscatedCode} + module.exports; + `); + + const resultTrue = result.test(true); + assert.deepEqual(resultTrue, { bgcolor: 'test', color: 'ok' }); + + const resultFalse = result.test(false); + assert.deepEqual(resultFalse, { bgcolor: 'test' }); + } + }); + + it('does not break on run with transformObjectKeys enabled', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1372.js'); + + for (let i = 0; i < samplesCount; i++) { + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true, + stringArray: true, + stringArrayThreshold: 1 + }).getObfuscatedCode(); + + const result = eval(` + ${obfuscatedCode} + module.exports; + `); + + const resultTrue = result.test(true); + assert.deepEqual(resultTrue, { bgcolor: 'test', color: 'ok' }); + + const resultFalse = result.test(false); + assert.deepEqual(resultFalse, { bgcolor: 'test' }); + } + }); + + it('does not break on run with both controlFlowFlattening and transformObjectKeys enabled', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1372.js'); + + for (let i = 0; i < samplesCount; i++) { + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + controlFlowFlattening: true, + controlFlowFlatteningThreshold: 1, + transformObjectKeys: true, + stringArray: true, + stringArrayThreshold: 1 + }).getObfuscatedCode(); + + const result = eval(` + ${obfuscatedCode} + module.exports; + `); + + const resultTrue = result.test(true); + assert.deepEqual(resultTrue, { bgcolor: 'test', color: 'ok' }); + + const resultFalse = result.test(false); + assert.deepEqual(resultFalse, { bgcolor: 'test' }); + } + }); + }); +}); diff --git a/test/functional-tests/node-transformers/preparing-transformers/obfuscating-guards/conditional-comment-obfuscating-guard/ConditionalCommentObfuscatingGuard.spec.ts b/test/functional-tests/node-transformers/preparing-transformers/obfuscating-guards/conditional-comment-obfuscating-guard/ConditionalCommentObfuscatingGuard.spec.ts index c8822dfb0..bb4edbdbe 100644 --- a/test/functional-tests/node-transformers/preparing-transformers/obfuscating-guards/conditional-comment-obfuscating-guard/ConditionalCommentObfuscatingGuard.spec.ts +++ b/test/functional-tests/node-transformers/preparing-transformers/obfuscating-guards/conditional-comment-obfuscating-guard/ConditionalCommentObfuscatingGuard.spec.ts @@ -134,11 +134,11 @@ describe('ConditionalCommentObfuscatingGuard', () => { const ignoredFunctionExpression1RegExp: RegExp = /var bar *= *function *\(a, *b, *c\) *{/; const ignoredFunctionExpression2RegExp: RegExp = /var baz *= *function *\(a, *b, *c\) *{/; - const obfuscatedFunctionCallRegExp: RegExp = /_0x([a-f0-9]){5,6}\( *\);/g; + const obfuscatedFunctionCallRegExp: RegExp = /_0x([a-f0-9]){5,6}\( *\)[,;]/g; const expectedObfuscatedFunctionCallsLength: number = 3; - const ignoredFunctionCall1RegExp: RegExp = /bar\( *\);/; - const ignoredFunctionCall2RegExp: RegExp = /baz\( *\);/; + const ignoredFunctionCall1RegExp: RegExp = /bar\( *\)[,;]/; + const ignoredFunctionCall2RegExp: RegExp = /baz\( *\)[,;]/; let obfuscatedCode: string, obfuscatedFunctionExpressionMatchesLength: number, From 3848bca7941ed86d62e6a7108b960201613d2172 Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Tue, 27 Jan 2026 17:20:23 +0400 Subject: [PATCH 11/43] Backport some fixes from Pro version (#1374) --- CHANGELOG.md | 2 + src/analyzers/scope-analyzer/ScopeAnalyzer.ts | 114 ++++++++++++++++++ src/node/NodeUtils.ts | 5 + .../scope-analyzer/ScopeAnalyzer.spec.ts | 78 ++++++++++++ .../fixtures/annex-b-function-hoisting.js | 39 ++++++ .../fixtures/annex-b-let-const-shadowing.js | 9 ++ .../fixtures/annex-b-strict-mode.js | 11 ++ ...ObjectPatternPropertiesTransformer.spec.ts | 59 +++++++++ .../destructuring-default-outer-var.js | 8 ++ .../nested-destructuring-default-outer-var.js | 8 ++ .../simple-default-param-outer-var.js | 8 ++ test/helpers/evalLocal.ts | 9 ++ 12 files changed, 350 insertions(+) create mode 100644 test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-function-hoisting.js create mode 100644 test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-let-const-shadowing.js create mode 100644 test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-strict-mode.js create mode 100644 test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/destructuring-default-outer-var.js create mode 100644 test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/nested-destructuring-default-outer-var.js create mode 100644 test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/simple-default-param-outer-var.js create mode 100644 test/helpers/evalLocal.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c289cddab..8fbbaf6f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ v5.2.0 --- * Skip obfuscation of `process.env.*` * Fixed `controlFlowFlattening` breaking short-circuit evaluation with spread operator and conditional objects. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1372 +* Fix Annex B function hoisting: block-scoped function declarations are now correctly linked to references outside the block in non-strict mode +* Fixed `NodeUtils.cloneRecursive` corrupting `range` property when cloning AST nodes, causing scope analysis to incorrectly resolve destructuring default parameter references v5.1.0 --- diff --git a/src/analyzers/scope-analyzer/ScopeAnalyzer.ts b/src/analyzers/scope-analyzer/ScopeAnalyzer.ts index 169e33397..b4001cf6d 100644 --- a/src/analyzers/scope-analyzer/ScopeAnalyzer.ts +++ b/src/analyzers/scope-analyzer/ScopeAnalyzer.ts @@ -84,6 +84,11 @@ export class ScopeAnalyzer implements IScopeAnalyzer { sourceType: ScopeAnalyzer.sourceTypes[i] }); + // Fix Annex B function hoisting references + // eslint-scope doesn't implement Annex B semantics where function declarations + // in blocks also create a var-hoisted binding in the enclosing function scope + this.fixAnnexBFunctionHoisting(); + return; } catch (error) { if (i < sourceTypeLength - 1) { @@ -117,6 +122,101 @@ export class ScopeAnalyzer implements IScopeAnalyzer { return scope; } + /** + * Fix Annex B function hoisting references. + * + * In non-strict mode, function declarations in blocks have dual binding: + * 1. A block-scoped binding (handled by eslint-scope) + * 2. A var-hoisted binding in the enclosing function scope (NOT handled by eslint-scope) + * + * This method merges block-scoped function declarations into the enclosing + * function scope and links unresolved references. + */ + private fixAnnexBFunctionHoisting(): void { + if (!this.scopeManager) { + return; + } + + this.walkScopes(this.scopeManager.globalScope, (scope: eslintScope.Scope) => { + if (scope.type !== 'block' && scope.type !== 'switch') { + return; + } + + // Skip strict mode scopes - Annex B doesn't apply + if (scope.isStrict) { + return; + } + + const functionScope = scope.variableScope; + + if (!functionScope) { + return; + } + + for (let i = scope.variables.length - 1; i >= 0; i--) { + const variable = scope.variables[i]; + + const isFunctionDeclaration = variable.defs.some( + (def) => def.type === 'FunctionName' && def.node?.type === 'FunctionDeclaration' + ); + + if (!isFunctionDeclaration) { + continue; + } + + // Find existing variable with the same name in function scope (shadowing case) + const outerVariable = functionScope.variables.find((v) => v.name === variable.name && v !== variable); + + // Per Annex B.3.3, hoisting only applies if outer binding is var/function (not let/const) + const isOuterLetOrConst = outerVariable?.defs.some( + (def) => def.type === 'Variable' && (def.parent?.kind === 'let' || def.parent?.kind === 'const') + ); + + // Skip Annex B hoisting if there's a let/const with the same name + if (isOuterLetOrConst) { + continue; + } + + const targetVariable = outerVariable ?? variable; + + if (outerVariable) { + // Merge inner function's identifiers and references into outer + outerVariable.identifiers.push(...variable.identifiers); + outerVariable.references.push(...variable.references); + } else { + // Move variable to function scope so references can find it + functionScope.variables.push(variable); + } + + // Remove from block scope + scope.variables.splice(i, 1); + + // Link "through" references with matching name to the target variable + this.linkThroughReferences(variable.name, functionScope, targetVariable); + } + }); + } + + /** + * Link unresolved "through" references to a variable. + * + * @param {string} name - The variable name to match + * @param {Scope} scope - The scope to start searching from + * @param {Variable} targetVariable - The variable to link references to + */ + private linkThroughReferences(name: string, scope: eslintScope.Scope, targetVariable: eslintScope.Variable): void { + for (let i = scope.through.length - 1; i >= 0; i--) { + if (scope.through[i].identifier.name === name) { + targetVariable.references.push(scope.through[i]); + scope.through.splice(i, 1); + } + } + + for (const childScope of scope.childScopes) { + this.linkThroughReferences(name, childScope, targetVariable); + } + } + /** * @param {Scope} scope */ @@ -150,4 +250,18 @@ export class ScopeAnalyzer implements IScopeAnalyzer { this.sanitizeScopes(childScope); } } + + /** + * Walk through all scopes in the scope tree + * + * @param {Scope} scope - Starting scope + * @param {Function} callback - Function to call for each scope + */ + private walkScopes(scope: eslintScope.Scope, callback: (scope: eslintScope.Scope) => void): void { + callback(scope); + + for (const childScope of scope.childScopes) { + this.walkScopes(childScope, callback); + } + } } diff --git a/src/node/NodeUtils.ts b/src/node/NodeUtils.ts index 2834fb661..11cf6f305 100644 --- a/src/node/NodeUtils.ts +++ b/src/node/NodeUtils.ts @@ -122,6 +122,11 @@ export class NodeUtils { return node; } + // Handle primitives directly - don't try to clone them as objects + if (typeof node !== 'object') { + return node; + } + const copy: Partial = {}; const nodeKeys: (keyof T)[] = <(keyof T)[]>Object.keys(node); diff --git a/test/functional-tests/analyzers/scope-analyzer/ScopeAnalyzer.spec.ts b/test/functional-tests/analyzers/scope-analyzer/ScopeAnalyzer.spec.ts index b0f82d724..188f6d0cc 100644 --- a/test/functional-tests/analyzers/scope-analyzer/ScopeAnalyzer.spec.ts +++ b/test/functional-tests/analyzers/scope-analyzer/ScopeAnalyzer.spec.ts @@ -2,6 +2,7 @@ import 'reflect-metadata'; import { assert } from 'chai'; +import { evalLocal } from '../../../helpers/evalLocal'; import { readFileAsString } from '../../../helpers/readFileAsString'; import { JavaScriptObfuscator } from '../../../../src/JavaScriptObfuscatorFacade'; @@ -43,5 +44,82 @@ describe('ScopeAnalyzer', () => { assert.equal(error, null); }); }); + + describe('Variant #2: Annex B function hoisting', () => { + describe('Variant #1: basic block-scoped function hoisting', () => { + const samplesCount: number = 50; + + let testFunc: () => void; + + beforeEach(() => { + const code: string = readFileAsString(__dirname + '/fixtures/annex-b-function-hoisting.js'); + + testFunc = () => { + for (let i = 0; i < samplesCount; i++) { + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + seed: i + }).getObfuscatedCode(); + + const result = evalLocal(obfuscatedCode); + + if (result.test1 !== 'foo') { + throw new Error('test1 failed: expected foo, got ' + result.test1); + } + + if (result.test2 !== 'bar') { + throw new Error('test2 failed: expected bar, got ' + result.test2); + } + } + }; + }); + + it('should correctly handle Annex B function hoisting references', () => { + assert.doesNotThrow(testFunc); + }); + }); + + describe('Variant #2: strict mode should not apply Annex B hoisting', () => { + let testFunc: () => void; + + beforeEach(() => { + const code: string = readFileAsString(__dirname + '/fixtures/annex-b-strict-mode.js'); + + testFunc = () => { + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + seed: 12345 + }).getObfuscatedCode(); + + eval(obfuscatedCode); + }; + }); + + it('should correctly handle strict mode block-scoped functions', () => { + assert.doesNotThrow(testFunc); + }); + }); + + describe('Variant #3: let/const shadowing should prevent Annex B hoisting', () => { + let testFunc: () => void; + + beforeEach(() => { + const code: string = readFileAsString(__dirname + '/fixtures/annex-b-let-const-shadowing.js'); + + testFunc = () => { + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + seed: 12345 + }).getObfuscatedCode(); + + const result = eval(obfuscatedCode); + if (result !== 'outer') { + throw new Error('Expected outer, got: ' + result); + } + }; + }); + + it('should not hoist when let/const shadows the function name', () => { + assert.doesNotThrow(testFunc); + }); + }); + }); }); }); diff --git a/test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-function-hoisting.js b/test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-function-hoisting.js new file mode 100644 index 000000000..0dc18de10 --- /dev/null +++ b/test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-function-hoisting.js @@ -0,0 +1,39 @@ +// Basic Annex B case: function in block referenced after block +function test1() { + if (true) { + function foo() { + return 'foo'; + } + } + return foo(); +} + +// Function in switch case +function test2(x) { + switch (x) { + case 1: + function bar() { + return 'bar'; + } + break; + } + return bar(); +} + +// Multiple blocks with same function name +function test3() { + if (true) { + function baz() { + return 'first'; + } + } + if (false) { + function baz() { + return 'second'; + } + } + return baz(); +} + +// Return results for testing +({ test1: test1(), test2: test2(1) }); diff --git a/test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-let-const-shadowing.js b/test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-let-const-shadowing.js new file mode 100644 index 000000000..fee0550b8 --- /dev/null +++ b/test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-let-const-shadowing.js @@ -0,0 +1,9 @@ +function test() { + let foo = 'outer'; + if (true) { + function foo() { return 'inner'; } + foo(); // block-scoped foo + } + return foo; // should be 'outer', not the function +} +test(); diff --git a/test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-strict-mode.js b/test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-strict-mode.js new file mode 100644 index 000000000..6bff144bf --- /dev/null +++ b/test/functional-tests/analyzers/scope-analyzer/fixtures/annex-b-strict-mode.js @@ -0,0 +1,11 @@ +'use strict'; +function test() { + let foo; + if (true) { + function foo() { return 'inner'; } + foo(); // This refers to block-scoped foo + } + // foo here is the outer let, which is undefined + return typeof foo; +} +test(); diff --git a/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/ObjectPatternPropertiesTransformer.spec.ts b/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/ObjectPatternPropertiesTransformer.spec.ts index 69a870363..dd43acb1d 100644 --- a/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/ObjectPatternPropertiesTransformer.spec.ts +++ b/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/ObjectPatternPropertiesTransformer.spec.ts @@ -3,6 +3,7 @@ import { assert } from 'chai'; import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes'; import { readFileAsString } from '../../../../helpers/readFileAsString'; +import { evalLocal } from '../../../../helpers/evalLocal'; import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade'; @@ -142,4 +143,62 @@ describe('ObjectPatternPropertiesTransformer', () => { }); }); }); + + describe('Variant #3: destructuring default parameter with var shadowing', () => { + describe('Variant #1: destructuring default should reference outer var, not inner var', () => { + let code: string; + let obfuscatedCode: string; + + before(() => { + code = readFileAsString(__dirname + '/fixtures/destructuring-default-outer-var.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should correctly resolve destructuring default to outer variable', () => { + // Default parameter `a = x` should use outer 'x' value ('outer'), + // NOT the inner var x = 'inner' which is in the function body scope + assert.equal(evalLocal(code), 'outer'); + assert.equal(evalLocal(obfuscatedCode), evalLocal(code)); + }); + }); + + describe('Variant #2: nested destructuring default should reference outer var', () => { + let code: string; + let obfuscatedCode: string; + + before(() => { + code = readFileAsString(__dirname + '/fixtures/nested-destructuring-default-outer-var.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should correctly resolve nested destructuring default to outer variable', () => { + assert.equal(evalLocal(code), 'outer'); + assert.equal(evalLocal(obfuscatedCode), evalLocal(code)); + }); + }); + + describe('Variant #3: simple default param with var shadow', () => { + let code: string; + let obfuscatedCode: string; + + before(() => { + code = readFileAsString(__dirname + '/fixtures/simple-default-param-outer-var.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should correctly resolve default param to outer variable', () => { + assert.equal(evalLocal(code), 'outer'); + assert.equal(evalLocal(obfuscatedCode), evalLocal(code)); + }); + }); + }); }); diff --git a/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/destructuring-default-outer-var.js b/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/destructuring-default-outer-var.js new file mode 100644 index 000000000..614804148 --- /dev/null +++ b/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/destructuring-default-outer-var.js @@ -0,0 +1,8 @@ +(function() { + var x = 'outer'; + function f({ a = x } = {}) { + var x = 'inner'; + return a; + } + return f(); +})(); diff --git a/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/nested-destructuring-default-outer-var.js b/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/nested-destructuring-default-outer-var.js new file mode 100644 index 000000000..bf8d3fa64 --- /dev/null +++ b/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/nested-destructuring-default-outer-var.js @@ -0,0 +1,8 @@ +(function() { + var x = 'outer'; + function f({ a: { b = x } } = { a: {} }) { + var x = 'inner'; + return b; + } + return f(); +})(); diff --git a/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/simple-default-param-outer-var.js b/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/simple-default-param-outer-var.js new file mode 100644 index 000000000..926fe3fec --- /dev/null +++ b/test/functional-tests/node-transformers/converting-transformers/object-pattern-properties-transformer/fixtures/simple-default-param-outer-var.js @@ -0,0 +1,8 @@ +(function() { + var x = 'outer'; + function f(a = x) { + var x = 'inner'; + return a; + } + return f(); +})(); diff --git a/test/helpers/evalLocal.ts b/test/helpers/evalLocal.ts new file mode 100644 index 000000000..237c669ac --- /dev/null +++ b/test/helpers/evalLocal.ts @@ -0,0 +1,9 @@ +/** + * Evaluates code using indirect eval. + * Indirect eval runs in global scope without inheriting strict mode from the calling context. + * This is needed for testing features like Annex B function hoisting. + * + * @param {string} code + * @returns {any} + */ +export const evalLocal = (code: string): any => (0, eval)(code); From 44028a147f90704d5cbb40ee13e6248b3f83e413 Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Tue, 27 Jan 2026 22:19:27 +0400 Subject: [PATCH 12/43] Fixed `transformObjectKeys` incorrectly hoisting object literal outsie of loop (#1375) --- CHANGELOG.md | 4 + package.json | 2 +- .../ObjectExpressionKeysTransformer.ts | 36 ++++++++- src/node/NodeGuards.ts | 21 +++++ .../issues/fixtures/issue1300-do-while.js | 9 +++ .../issues/fixtures/issue1300-for-in.js | 8 ++ .../issues/fixtures/issue1300-for-of.js | 7 ++ .../issues/fixtures/issue1300-while.js | 8 ++ .../issues/fixtures/issue1300.js | 8 ++ .../functional-tests/issues/issue1300.spec.ts | 79 +++++++++++++++++++ .../ObjectExpressionKeysTransformer.spec.ts | 24 +++--- 11 files changed, 192 insertions(+), 14 deletions(-) create mode 100644 test/functional-tests/issues/fixtures/issue1300-do-while.js create mode 100644 test/functional-tests/issues/fixtures/issue1300-for-in.js create mode 100644 test/functional-tests/issues/fixtures/issue1300-for-of.js create mode 100644 test/functional-tests/issues/fixtures/issue1300-while.js create mode 100644 test/functional-tests/issues/fixtures/issue1300.js create mode 100644 test/functional-tests/issues/issue1300.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fbbaf6f5..f64fba88e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.2.1 +--- +* Fixed `transformObjectKeys` incorrectly hoisting object literal outside of loop when loop body is a single statement without braces, causing all iterations to share the same object reference. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1300 + v5.2.0 --- * Skip obfuscation of `process.env.*` diff --git a/package.json b/package.json index 7bb02fa54..4baf28c3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.2.0", + "version": "5.2.1", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", diff --git a/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts b/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts index e0addf21f..05bc0af6c 100644 --- a/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts +++ b/src/node-transformers/converting-transformers/ObjectExpressionKeysTransformer.ts @@ -127,7 +127,8 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer { ObjectExpressionKeysTransformer.isProhibitedSequenceExpression( objectExpressionNode, objectExpressionHostStatement - ) + ) || + ObjectExpressionKeysTransformer.isProhibitedLoopBody(objectExpressionNode) ) { return true; } @@ -140,6 +141,39 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer { return hasReferencedIdentifier || hasCallExpression; } + /** + * @param {ObjectExpression} objectExpressionNode + * @returns {boolean} + */ + private static isProhibitedLoopBody(objectExpressionNode: ESTree.ObjectExpression): boolean { + let currentNode: ESTree.Node | undefined = objectExpressionNode; + + while (currentNode) { + const parentNode: ESTree.Node | undefined = currentNode.parentNode; + + if (!parentNode || parentNode === currentNode) { + break; + } + + const isNonBlockLoopBody: boolean = + NodeGuards.isLoopStatementNode(parentNode) && + parentNode.body === currentNode && + !NodeGuards.isBlockStatementNode(currentNode); + + if (isNonBlockLoopBody) { + return true; + } + + if (NodeGuards.isFunctionNode(parentNode) || NodeGuards.isProgramNode(parentNode)) { + break; + } + + currentNode = parentNode; + } + + return false; + } + /** * @param {ObjectExpression} objectExpressionNode * @param {Node} objectExpressionNodeParentNode diff --git a/src/node/NodeGuards.ts b/src/node/NodeGuards.ts index b34962a39..da88ffecd 100644 --- a/src/node/NodeGuards.ts +++ b/src/node/NodeGuards.ts @@ -330,6 +330,27 @@ export class NodeGuards { return node.type === NodeType.LogicalExpression; } + /** + * @param {Node} node + * @returns {boolean} + */ + public static isLoopStatementNode( + node: ESTree.Node + ): node is + | ESTree.ForStatement + | ESTree.ForInStatement + | ESTree.ForOfStatement + | ESTree.WhileStatement + | ESTree.DoWhileStatement { + return ( + NodeGuards.isForStatementNode(node) || + NodeGuards.isForInStatementNode(node) || + NodeGuards.isForOfStatementNode(node) || + NodeGuards.isWhileStatementNode(node) || + NodeGuards.isDoWhileStatementNode(node) + ); + } + /** * @param {Node} node * @returns {boolean} diff --git a/test/functional-tests/issues/fixtures/issue1300-do-while.js b/test/functional-tests/issues/fixtures/issue1300-do-while.js new file mode 100644 index 000000000..ddb2f0380 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1300-do-while.js @@ -0,0 +1,9 @@ +(function() { + let arr = []; + let i = 0; + do + arr.push({value: 0}); + while (++i < 3); + arr[0].value = 1; + return arr[0] === arr[1]; +})(); diff --git a/test/functional-tests/issues/fixtures/issue1300-for-in.js b/test/functional-tests/issues/fixtures/issue1300-for-in.js new file mode 100644 index 000000000..162f0e7c1 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1300-for-in.js @@ -0,0 +1,8 @@ +(function() { + let arr = []; + let obj = {a: 1, b: 2, c: 3}; + for (let key in obj) + arr.push({value: 0}); + arr[0].value = 1; + return arr[0] === arr[1]; +})(); diff --git a/test/functional-tests/issues/fixtures/issue1300-for-of.js b/test/functional-tests/issues/fixtures/issue1300-for-of.js new file mode 100644 index 000000000..01c3d6d67 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1300-for-of.js @@ -0,0 +1,7 @@ +(function() { + let arr = []; + for (let x of [1, 2, 3]) + arr.push({value: 0}); + arr[0].value = 1; + return arr[0] === arr[1]; +})(); diff --git a/test/functional-tests/issues/fixtures/issue1300-while.js b/test/functional-tests/issues/fixtures/issue1300-while.js new file mode 100644 index 000000000..e64e93267 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1300-while.js @@ -0,0 +1,8 @@ +(function() { + let arr = []; + let i = 0; + while (i++ < 3) + arr.push({value: 0}); + arr[0].value = 1; + return arr[0] === arr[1]; +})(); diff --git a/test/functional-tests/issues/fixtures/issue1300.js b/test/functional-tests/issues/fixtures/issue1300.js new file mode 100644 index 000000000..7d9fdc1c9 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1300.js @@ -0,0 +1,8 @@ +// Object inside for loop should create new object each iteration +(function() { + let arr = []; + for (let i = 0; i < 3; i++) + arr.push({value: 0}); + arr[0].value = 1; + return arr[0] === arr[1]; // should be false +})(); diff --git a/test/functional-tests/issues/issue1300.spec.ts b/test/functional-tests/issues/issue1300.spec.ts new file mode 100644 index 000000000..612be091a --- /dev/null +++ b/test/functional-tests/issues/issue1300.spec.ts @@ -0,0 +1,79 @@ +import { assert } from 'chai'; +import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes'; +import { readFileAsString } from '../../helpers/readFileAsString'; +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +// +// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1300 +// +describe('Issue #1300', () => { + describe('Object inside loop should create new object each iteration', () => { + const samplesCount = 50; + + it('does not break object creation semantics with transformObjectKeys', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1300.js'); + + for (let i = 0; i < samplesCount; i++) { + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true, + seed: i + }).getObfuscatedCode(); + + const originalResult = eval(code); + const obfuscatedResult = eval(obfuscatedCode); + + assert.equal(originalResult, false, 'Original code should return false'); + assert.equal(obfuscatedResult, false, `Obfuscated code should return false (seed: ${i})`); + } + }); + + it('does not break with for-in loop', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1300-for-in.js'); + + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true + }).getObfuscatedCode(); + + assert.equal(eval(code), false); + assert.equal(eval(obfuscatedCode), false); + }); + + it('does not break with for-of loop', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1300-for-of.js'); + + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true + }).getObfuscatedCode(); + + assert.equal(eval(code), false); + assert.equal(eval(obfuscatedCode), false); + }); + + it('does not break with while loop', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1300-while.js'); + + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true + }).getObfuscatedCode(); + + assert.equal(eval(code), false); + assert.equal(eval(obfuscatedCode), false); + }); + + it('does not break with do-while loop', () => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1300-do-while.js'); + + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + transformObjectKeys: true + }).getObfuscatedCode(); + + assert.equal(eval(code), false); + assert.equal(eval(obfuscatedCode), false); + }); + }); +}); diff --git a/test/functional-tests/node-transformers/converting-transformers/object-expression-keys-transformer/ObjectExpressionKeysTransformer.spec.ts b/test/functional-tests/node-transformers/converting-transformers/object-expression-keys-transformer/ObjectExpressionKeysTransformer.spec.ts index d261014a1..41b4e9eb9 100644 --- a/test/functional-tests/node-transformers/converting-transformers/object-expression-keys-transformer/ObjectExpressionKeysTransformer.spec.ts +++ b/test/functional-tests/node-transformers/converting-transformers/object-expression-keys-transformer/ObjectExpressionKeysTransformer.spec.ts @@ -1092,13 +1092,13 @@ describe('ObjectExpressionKeysTransformer', () => { }); describe('Variant #2: without block statement', () => { + // Object should NOT be transformed when inside loop without block statement + // to prevent all iterations sharing the same object reference (issue #1300) const match: string = `` + `var ${variableMatch};` + - `var ${variableMatch} *= *{};` + - `${variableMatch}\\['bar'] *= *'bar';` + `for *\\(var ${variableMatch} *= *0x0; *${variableMatch} *< *0xa; *${variableMatch}\\+\\+\\) *` + - `${variableMatch} *= *${variableMatch};` + + `${variableMatch} *= *\\{'bar': *'bar'\\};` + ``; const regExp: RegExp = new RegExp(match); @@ -1151,13 +1151,13 @@ describe('ObjectExpressionKeysTransformer', () => { }); describe('Variant #2: without block statement', () => { + // Object should NOT be transformed when inside loop without block statement + // to prevent all iterations sharing the same object reference (issue #1300) const match: string = `` + `var ${variableMatch} *= *{};` + - `var ${variableMatch} *= *{};` + - `${variableMatch}\\['bar'] *= *'bar';` + `for *\\(var ${variableMatch} in *${variableMatch}\\) *` + - `${variableMatch} *= *${variableMatch};` + + `${variableMatch} *= *\\{'bar': *'bar'\\};` + ``; const regExp: RegExp = new RegExp(match); @@ -1210,13 +1210,13 @@ describe('ObjectExpressionKeysTransformer', () => { }); describe('Variant #2: without block statement', () => { + // Object should NOT be transformed when inside loop without block statement + // to prevent all iterations sharing the same object reference (issue #1300) const match: string = `` + `var ${variableMatch} *= *\\[];` + - `var ${variableMatch} *= *{};` + - `${variableMatch}\\['bar'] *= *'bar';` + `for *\\(var ${variableMatch} of *${variableMatch}\\) *` + - `${variableMatch} *= *${variableMatch};` + + `${variableMatch} *= *\\{'bar': *'bar'\\};` + ``; const regExp: RegExp = new RegExp(match); @@ -1268,13 +1268,13 @@ describe('ObjectExpressionKeysTransformer', () => { }); describe('Variant #2: without block statement', () => { + // Object should NOT be transformed when inside loop without block statement + // to prevent all iterations sharing the same object reference (issue #1300) const match: string = `` + `var ${variableMatch};` + - `var ${variableMatch} *= *{};` + - `${variableMatch}\\['bar'] *= *'bar';` + `while *\\(!!\\[]\\)` + - `${variableMatch} *= *${variableMatch};` + + `${variableMatch} *= *\\{'bar': *'bar'\\};` + ``; const regExp: RegExp = new RegExp(match); From ccece3fef175f2589b347e6811ec13429652e279 Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Tue, 27 Jan 2026 22:59:28 +0400 Subject: [PATCH 13/43] Fixed parsing error when `await` is used as an identifier in non-async context (#1376) --- CHANGELOG.md | 1 + src/ASTParserFacade.ts | 6 ++- .../fixtures/issue1127-top-level-await.js | 2 + .../issues/fixtures/issue1127.js | 1 + .../functional-tests/issues/issue1127.spec.ts | 43 +++++++++++++++++++ 5 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/functional-tests/issues/fixtures/issue1127-top-level-await.js create mode 100644 test/functional-tests/issues/fixtures/issue1127.js create mode 100644 test/functional-tests/issues/issue1127.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f64fba88e..517c85999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Change Log v5.2.1 --- * Fixed `transformObjectKeys` incorrectly hoisting object literal outside of loop when loop body is a single statement without braces, causing all iterations to share the same object reference. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1300 +* Fixed parsing error when `await` is used as an identifier in non-async context. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1127 v5.2.0 --- diff --git a/src/ASTParserFacade.ts b/src/ASTParserFacade.ts index 796f2ad8b..8e3c27bfb 100644 --- a/src/ASTParserFacade.ts +++ b/src/ASTParserFacade.ts @@ -58,7 +58,11 @@ export class ASTParserFacade { const comments: ESTree.Comment[] = []; const config: acorn.Options = { ...inputConfig, - allowAwaitOutsideFunction: true, + allowAwaitOutsideFunction: false, + allowReserved: true, + allowImportExportEverywhere: true, + allowReturnOutsideFunction: true, + allowSuperOutsideMethod: true, onComment: comments, sourceType }; diff --git a/test/functional-tests/issues/fixtures/issue1127-top-level-await.js b/test/functional-tests/issues/fixtures/issue1127-top-level-await.js new file mode 100644 index 000000000..9425538e2 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1127-top-level-await.js @@ -0,0 +1,2 @@ +const x = await Promise.resolve(1); +console.log(x); diff --git a/test/functional-tests/issues/fixtures/issue1127.js b/test/functional-tests/issues/fixtures/issue1127.js new file mode 100644 index 000000000..302fcad13 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1127.js @@ -0,0 +1 @@ +try { await; } catch { console.log('caught'); } diff --git a/test/functional-tests/issues/issue1127.spec.ts b/test/functional-tests/issues/issue1127.spec.ts new file mode 100644 index 000000000..bc95dba2a --- /dev/null +++ b/test/functional-tests/issues/issue1127.spec.ts @@ -0,0 +1,43 @@ +import { assert } from 'chai'; +import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes'; +import { readFileAsString } from '../../helpers/readFileAsString'; +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +// +// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1127 +// +describe('Issue #1127', () => { + describe('`await` used as identifier should not crash', () => { + let testFunc: () => string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1127.js'); + + testFunc = () => + JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('does not crash on obfuscating', () => { + assert.doesNotThrow(testFunc); + }); + }); + + describe('top-level await should still work', () => { + let testFunc: () => string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1127-top-level-await.js'); + + testFunc = () => + JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('does not crash on obfuscating', () => { + assert.doesNotThrow(testFunc); + }); + }); +}); From 05aacd9dfe593c706a6af3a19a2701ce7369161c Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Tue, 27 Jan 2026 23:34:22 +0400 Subject: [PATCH 14/43] Fixed `deadCodeInjection` causing SyntaxError when `arguments` from collected block statements was injected into class field initializers or static initialization blocks (#1377) --- CHANGELOG.md | 1 + .../DeadCodeInjectionTransformer.ts | 5 +- .../issues/fixtures/issue1166-static-block.js | 33 ++++++++++++ .../issues/fixtures/issue1166.js | 33 ++++++++++++ .../functional-tests/issues/issue1166.spec.ts | 53 +++++++++++++++++++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 test/functional-tests/issues/fixtures/issue1166-static-block.js create mode 100644 test/functional-tests/issues/fixtures/issue1166.js create mode 100644 test/functional-tests/issues/issue1166.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 517c85999..52c7bb10e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ v5.2.1 --- * Fixed `transformObjectKeys` incorrectly hoisting object literal outside of loop when loop body is a single statement without braces, causing all iterations to share the same object reference. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1300 * Fixed parsing error when `await` is used as an identifier in non-async context. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1127 +* Fixed `deadCodeInjection` causing SyntaxError when `arguments` from collected block statements was injected into class field initializers or static initialization blocks. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1166 v5.2.0 --- diff --git a/src/node-transformers/dead-code-injection-transformers/DeadCodeInjectionTransformer.ts b/src/node-transformers/dead-code-injection-transformers/DeadCodeInjectionTransformer.ts index d9508e327..cb706d338 100644 --- a/src/node-transformers/dead-code-injection-transformers/DeadCodeInjectionTransformer.ts +++ b/src/node-transformers/dead-code-injection-transformers/DeadCodeInjectionTransformer.ts @@ -101,6 +101,7 @@ export class DeadCodeInjectionTransformer extends AbstractNodeTransformer { * @param {Node} targetNode * @returns {boolean} */ + // eslint-disable-next-line complexity private static isProhibitedNodeInsideCollectedBlockStatement(targetNode: ESTree.Node): boolean { return ( NodeGuards.isFunctionDeclarationNode(targetNode) || // can break code on strict mode @@ -110,7 +111,9 @@ export class DeadCodeInjectionTransformer extends AbstractNodeTransformer { NodeGuards.isYieldExpressionNode(targetNode) || NodeGuards.isSuperNode(targetNode) || (NodeGuards.isForOfStatementNode(targetNode) && targetNode.await) || - NodeGuards.isPrivateIdentifierNode(targetNode) + NodeGuards.isPrivateIdentifierNode(targetNode) || + // `arguments` is not allowed in class field initializers or static initialization blocks + (NodeGuards.isIdentifierNode(targetNode) && targetNode.name === 'arguments') ); } diff --git a/test/functional-tests/issues/fixtures/issue1166-static-block.js b/test/functional-tests/issues/fixtures/issue1166-static-block.js new file mode 100644 index 000000000..02f37b467 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1166-static-block.js @@ -0,0 +1,33 @@ +// Function that uses `arguments` - this block can be collected for dead code injection +function logArgs() { + console.log(arguments); + console.log(arguments.length); +} + +function foo() { + console.log(arguments[0]); +} + +function bar() { + var args = arguments; + return args; +} + +// Class with static initialization block - dead code should NOT be injected here with `arguments` +class MyClass { + static value; + + static { + console.log('static block'); + MyClass.value = 42; + } + + method() { + console.log('method'); + } +} + +console.log(MyClass.value); +logArgs(1, 2, 3); +foo('test'); +bar('a', 'b'); diff --git a/test/functional-tests/issues/fixtures/issue1166.js b/test/functional-tests/issues/fixtures/issue1166.js new file mode 100644 index 000000000..572f89d87 --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1166.js @@ -0,0 +1,33 @@ +// Function that uses `arguments` - this block can be collected for dead code injection +function logArgs() { + console.log(arguments); + console.log(arguments.length); +} + +function foo() { + console.log(arguments[0]); +} + +function bar() { + var args = arguments; + return args; +} + +// Class with field initializers - dead code should NOT be injected here with `arguments` +class MyClass { + field1 = (() => { + console.log('initializer'); + return 1; + })(); + + field2 = 2; + + method() { + console.log('method'); + } +} + +new MyClass(); +logArgs(1, 2, 3); +foo('test'); +bar('a', 'b'); diff --git a/test/functional-tests/issues/issue1166.spec.ts b/test/functional-tests/issues/issue1166.spec.ts new file mode 100644 index 000000000..dad424c73 --- /dev/null +++ b/test/functional-tests/issues/issue1166.spec.ts @@ -0,0 +1,53 @@ +import { assert } from 'chai'; +import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes'; +import { readFileAsString } from '../../helpers/readFileAsString'; +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +// +// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1166 +// +describe('Issue #1166', () => { + describe('`arguments` in collected block statement should not be injected into class field initializer', () => { + let testFunc: () => string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1166.js'); + + testFunc = () => + JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + deadCodeInjection: true, + deadCodeInjectionThreshold: 1 + }).getObfuscatedCode(); + }); + + it('does not crash on obfuscating', () => { + // Run multiple times to increase chance of triggering the bug + for (let i = 0; i < 50; i++) { + assert.doesNotThrow(testFunc); + } + }); + }); + + describe('`arguments` in collected block statement should not be injected into static block', () => { + let testFunc: () => string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1166-static-block.js'); + + testFunc = () => + JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + deadCodeInjection: true, + deadCodeInjectionThreshold: 1 + }).getObfuscatedCode(); + }); + + it('does not crash on obfuscating', () => { + // Run multiple times to increase chance of triggering the bug + for (let i = 0; i < 50; i++) { + assert.doesNotThrow(testFunc); + } + }); + }); +}); From 999e93c084fe4477e026dab6762dace2dd8ccbbc Mon Sep 17 00:00:00 2001 From: sanex3339 Date: Tue, 27 Jan 2026 23:34:05 +0400 Subject: [PATCH 15/43] Fixed `transformObjectKeys` with `mangled` identifier generator causing variable shadowing when extracted object variable name matched an existing inner scope variable --- CHANGELOG.md | 1 + .../AbstractIdentifierNamesGenerator.ts | 24 ++++++++++++++ .../DictionaryIdentifierNamesGenerator.ts | 2 +- .../HexadecimalIdentifierNamesGenerator.ts | 2 +- .../MangledIdentifierNamesGenerator.ts | 2 +- .../issues/fixtures/issue1232.js | 1 + .../functional-tests/issues/issue1232.spec.ts | 32 +++++++++++++++++++ 7 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 test/functional-tests/issues/fixtures/issue1232.js create mode 100644 test/functional-tests/issues/issue1232.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c7bb10e..670232a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ v5.2.1 * Fixed `transformObjectKeys` incorrectly hoisting object literal outside of loop when loop body is a single statement without braces, causing all iterations to share the same object reference. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1300 * Fixed parsing error when `await` is used as an identifier in non-async context. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1127 * Fixed `deadCodeInjection` causing SyntaxError when `arguments` from collected block statements was injected into class field initializers or static initialization blocks. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1166 +* Fixed `transformObjectKeys` with `mangled` identifier generator causing variable shadowing when extracted object variable name matched an existing inner scope variable. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1232 v5.2.0 --- diff --git a/src/generators/identifier-names-generators/AbstractIdentifierNamesGenerator.ts b/src/generators/identifier-names-generators/AbstractIdentifierNamesGenerator.ts index 9779d6af7..161f1cc62 100644 --- a/src/generators/identifier-names-generators/AbstractIdentifierNamesGenerator.ts +++ b/src/generators/identifier-names-generators/AbstractIdentifierNamesGenerator.ts @@ -31,6 +31,11 @@ export abstract class AbstractIdentifierNamesGenerator implements IIdentifierNam */ protected readonly lexicalScopesPreservedNamesMap: WeakMap> = new WeakMap(); + /** + * @type {Set} + */ + protected readonly allLexicalScopePreservedNames: Set = new Set(); + /** * @param {IRandomGenerator} randomGenerator * @param {IOptions} options @@ -72,6 +77,8 @@ export abstract class AbstractIdentifierNamesGenerator implements IIdentifierNam preservedNamesForLexicalScopeSet.add(name); this.lexicalScopesPreservedNamesMap.set(lexicalScopeNode, preservedNamesForLexicalScopeSet); + + this.allLexicalScopePreservedNames.add(name); } /** @@ -108,6 +115,23 @@ export abstract class AbstractIdentifierNamesGenerator implements IIdentifierNam return true; } + /** + * Checks if the name is valid and not preserved in any scope (global or lexical). + * This is used for global scope name generation to avoid conflicts with + * variables in any lexical scope that might shadow the global variable. + * + * @param {string} name + * @returns {boolean} + */ + public isValidIdentifierNameInAllScopes(name: string): boolean { + if (!this.isValidIdentifierName(name)) { + return false; + } + + // Check if the name is preserved in any lexical scope + return !this.allLexicalScopePreservedNames.has(name); + } + /** * @param {string} name * @returns {boolean} diff --git a/src/generators/identifier-names-generators/DictionaryIdentifierNamesGenerator.ts b/src/generators/identifier-names-generators/DictionaryIdentifierNamesGenerator.ts index da6195a57..7d8b75a7e 100644 --- a/src/generators/identifier-names-generators/DictionaryIdentifierNamesGenerator.ts +++ b/src/generators/identifier-names-generators/DictionaryIdentifierNamesGenerator.ts @@ -86,7 +86,7 @@ export class DictionaryIdentifierNamesGenerator extends AbstractIdentifierNamesG const identifierName: string = this.generateNewDictionaryName((newIdentifierName: string) => { const identifierNameWithPrefix: string = `${prefix}${newIdentifierName}`; - return this.isValidIdentifierName(identifierNameWithPrefix); + return this.isValidIdentifierNameInAllScopes(identifierNameWithPrefix); }); const identifierNameWithPrefix = `${prefix}${identifierName}`; diff --git a/src/generators/identifier-names-generators/HexadecimalIdentifierNamesGenerator.ts b/src/generators/identifier-names-generators/HexadecimalIdentifierNamesGenerator.ts index b47629013..c6f87933e 100644 --- a/src/generators/identifier-names-generators/HexadecimalIdentifierNamesGenerator.ts +++ b/src/generators/identifier-names-generators/HexadecimalIdentifierNamesGenerator.ts @@ -43,7 +43,7 @@ export class HexadecimalIdentifierNamesGenerator extends AbstractIdentifierNames const baseIdentifierName: string = hexadecimalNumber.slice(0, baseNameLength); const identifierName: string = `_${baseIdentifierName}`; - if (!this.isValidIdentifierName(identifierName)) { + if (!this.isValidIdentifierNameInAllScopes(identifierName)) { return this.generateNext(nameLength); } diff --git a/src/generators/identifier-names-generators/MangledIdentifierNamesGenerator.ts b/src/generators/identifier-names-generators/MangledIdentifierNamesGenerator.ts index 1c011bda3..c5ca35c87 100644 --- a/src/generators/identifier-names-generators/MangledIdentifierNamesGenerator.ts +++ b/src/generators/identifier-names-generators/MangledIdentifierNamesGenerator.ts @@ -103,7 +103,7 @@ export class MangledIdentifierNamesGenerator extends AbstractIdentifierNamesGene (newIdentifierName: string) => { const identifierNameWithPrefix: string = `${prefix}${newIdentifierName}`; - return this.isValidIdentifierName(identifierNameWithPrefix); + return this.isValidIdentifierNameInAllScopes(identifierNameWithPrefix); } ); const identifierNameWithPrefix: string = `${prefix}${identifierName}`; diff --git a/test/functional-tests/issues/fixtures/issue1232.js b/test/functional-tests/issues/fixtures/issue1232.js new file mode 100644 index 000000000..45c0f819e --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1232.js @@ -0,0 +1 @@ +[].forEach(a => a === { a: 1 }); diff --git a/test/functional-tests/issues/issue1232.spec.ts b/test/functional-tests/issues/issue1232.spec.ts new file mode 100644 index 000000000..3b1375e86 --- /dev/null +++ b/test/functional-tests/issues/issue1232.spec.ts @@ -0,0 +1,32 @@ +import { assert } from 'chai'; +import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes'; +import { readFileAsString } from '../../helpers/readFileAsString'; +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +// +// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1232 +// +describe('Issue #1232', () => { + describe('transformObjectKeys should not cause variable shadowing with mangled identifiers', () => { + let obfuscatedCode: string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1232.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + identifierNamesGenerator: 'mangled', + transformObjectKeys: true + }).getObfuscatedCode(); + }); + + it('should not rename extracted object variable to same name as function parameter', () => { + const shadowingPattern = /(\w+)\s*===\s*\1[)\s;,]/; + + assert.isFalse( + shadowingPattern.test(obfuscatedCode), + `Variable shadowing detected in obfuscated code: ${obfuscatedCode}` + ); + }); + }); +}); From 6864328c0334e03513a449f703f8b0482e765510 Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Wed, 28 Jan 2026 01:04:40 +0400 Subject: [PATCH 16/43] Fix identifiers generation (#1378) --- ...ctExpressionVariableDeclarationHostNode.ts | 4 +- .../AbstractIdentifierNamesGenerator.ts | 6 ++ .../DictionaryIdentifierNamesGenerator.ts | 37 +++++++--- .../HexadecimalIdentifierNamesGenerator.ts | 67 +++++++++++++------ .../MangledIdentifierNamesGenerator.ts | 46 ++++++++----- .../IIdentifierNamesGenerator.ts | 12 ++++ 6 files changed, 126 insertions(+), 46 deletions(-) diff --git a/src/custom-nodes/object-expression-keys-transformer-nodes/ObjectExpressionVariableDeclarationHostNode.ts b/src/custom-nodes/object-expression-keys-transformer-nodes/ObjectExpressionVariableDeclarationHostNode.ts index 3bb223c95..dfdd1a152 100644 --- a/src/custom-nodes/object-expression-keys-transformer-nodes/ObjectExpressionVariableDeclarationHostNode.ts +++ b/src/custom-nodes/object-expression-keys-transformer-nodes/ObjectExpressionVariableDeclarationHostNode.ts @@ -58,8 +58,10 @@ export class ObjectExpressionVariableDeclarationHostNode extends AbstractCustomN * @returns {TStatement[]} */ protected getNodeStructure(): TStatement[] { + // Use generateForGlobalScopeWithAllScopesValidation when in global scope + // to avoid shadowing variables in inner lexical scopes const variableDeclarationName: string = NodeGuards.isProgramNode(this.lexicalScopeNode) - ? this.identifierNamesGenerator.generateForGlobalScope() + ? this.identifierNamesGenerator.generateForGlobalScopeWithAllScopesValidation() : this.identifierNamesGenerator.generateForLexicalScope(this.lexicalScopeNode); const structure: TStatement = NodeFactory.variableDeclarationNode( diff --git a/src/generators/identifier-names-generators/AbstractIdentifierNamesGenerator.ts b/src/generators/identifier-names-generators/AbstractIdentifierNamesGenerator.ts index 161f1cc62..a9655ad1f 100644 --- a/src/generators/identifier-names-generators/AbstractIdentifierNamesGenerator.ts +++ b/src/generators/identifier-names-generators/AbstractIdentifierNamesGenerator.ts @@ -150,6 +150,12 @@ export abstract class AbstractIdentifierNamesGenerator implements IIdentifierNam */ public abstract generateForGlobalScope(nameLength?: number): string; + /** + * @param {number} nameLength + * @returns {string} + */ + public abstract generateForGlobalScopeWithAllScopesValidation(nameLength?: number): string; + /** * @param {TNodeWithLexicalScope} lexicalScopeNode * @param {number} nameLength diff --git a/src/generators/identifier-names-generators/DictionaryIdentifierNamesGenerator.ts b/src/generators/identifier-names-generators/DictionaryIdentifierNamesGenerator.ts index 7d8b75a7e..4db36fccd 100644 --- a/src/generators/identifier-names-generators/DictionaryIdentifierNamesGenerator.ts +++ b/src/generators/identifier-names-generators/DictionaryIdentifierNamesGenerator.ts @@ -81,18 +81,14 @@ export class DictionaryIdentifierNamesGenerator extends AbstractIdentifierNamesG * @returns {string} */ public generateForGlobalScope(): string { - const prefix: string = this.options.identifiersPrefix ? `${this.options.identifiersPrefix}` : ''; - - const identifierName: string = this.generateNewDictionaryName((newIdentifierName: string) => { - const identifierNameWithPrefix: string = `${prefix}${newIdentifierName}`; - - return this.isValidIdentifierNameInAllScopes(identifierNameWithPrefix); - }); - const identifierNameWithPrefix = `${prefix}${identifierName}`; - - this.preserveName(identifierNameWithPrefix); + return this.generateForGlobalScopeInternal((name) => this.isValidIdentifierName(name)); + } - return identifierNameWithPrefix; + /** + * @returns {string} + */ + public generateForGlobalScopeWithAllScopesValidation(): string { + return this.generateForGlobalScopeInternal((name) => this.isValidIdentifierNameInAllScopes(name)); } /** @@ -121,6 +117,25 @@ export class DictionaryIdentifierNamesGenerator extends AbstractIdentifierNamesG return this.generateNewDictionaryName(); } + /** + * @param {(name: string) => boolean} validationFn + * @returns {string} + */ + private generateForGlobalScopeInternal(validationFn: (name: string) => boolean): string { + const prefix: string = this.options.identifiersPrefix ? `${this.options.identifiersPrefix}` : ''; + + const identifierName: string = this.generateNewDictionaryName((newIdentifierName: string) => { + const identifierNameWithPrefix: string = `${prefix}${newIdentifierName}`; + + return validationFn(identifierNameWithPrefix); + }); + const identifierNameWithPrefix = `${prefix}${identifierName}`; + + this.preserveName(identifierNameWithPrefix); + + return identifierNameWithPrefix; + } + /** * @param {(newIdentifierName: string) => boolean} validationFunction * @returns {string} diff --git a/src/generators/identifier-names-generators/HexadecimalIdentifierNamesGenerator.ts b/src/generators/identifier-names-generators/HexadecimalIdentifierNamesGenerator.ts index c6f87933e..59df523e5 100644 --- a/src/generators/identifier-names-generators/HexadecimalIdentifierNamesGenerator.ts +++ b/src/generators/identifier-names-generators/HexadecimalIdentifierNamesGenerator.ts @@ -33,23 +33,7 @@ export class HexadecimalIdentifierNamesGenerator extends AbstractIdentifierNames * @returns {string} */ public generateNext(nameLength?: number): string { - const rangeMinInteger: number = 10000; - const rangeMaxInteger: number = 99_999_999; - const randomInteger: number = this.randomGenerator.getRandomInteger(rangeMinInteger, rangeMaxInteger); - const hexadecimalNumber: string = NumberUtils.toHex(randomInteger); - const prefixLength: number = Utils.hexadecimalPrefix.length; - const baseNameLength: number = - (nameLength ?? HexadecimalIdentifierNamesGenerator.baseIdentifierNameLength) + prefixLength; - const baseIdentifierName: string = hexadecimalNumber.slice(0, baseNameLength); - const identifierName: string = `_${baseIdentifierName}`; - - if (!this.isValidIdentifierNameInAllScopes(identifierName)) { - return this.generateNext(nameLength); - } - - this.preserveName(identifierName); - - return identifierName; + return this.generateNextName(nameLength, (name) => this.isValidIdentifierName(name)); } /** @@ -57,9 +41,15 @@ export class HexadecimalIdentifierNamesGenerator extends AbstractIdentifierNames * @returns {string} */ public generateForGlobalScope(nameLength?: number): string { - const identifierName: string = this.generateNext(nameLength); + return this.generateForGlobalScopeInternal(nameLength, (name) => this.isValidIdentifierName(name)); + } - return `${this.options.identifiersPrefix}${identifierName}`.replace('__', '_'); + /** + * @param {number} nameLength + * @returns {string} + */ + public generateForGlobalScopeWithAllScopesValidation(nameLength?: number): string { + return this.generateForGlobalScopeInternal(nameLength, (name) => this.isValidIdentifierNameInAllScopes(name)); } /** @@ -79,4 +69,43 @@ export class HexadecimalIdentifierNamesGenerator extends AbstractIdentifierNames public generateForLabel(label: string, nameLength?: number): string { return this.generateNext(nameLength); } + + /** + * @param {number} nameLength + * @param {(name: string) => boolean} validationFn + * @returns {string} + */ + private generateForGlobalScopeInternal( + nameLength: number | undefined, + validationFn: (name: string) => boolean + ): string { + const identifierName: string = this.generateNextName(nameLength, validationFn); + + return `${this.options.identifiersPrefix}${identifierName}`.replace('__', '_'); + } + + /** + * @param {number} nameLength + * @param {(name: string) => boolean} validationFn + * @returns {string} + */ + private generateNextName(nameLength: number | undefined, validationFn: (name: string) => boolean): string { + const rangeMinInteger: number = 10000; + const rangeMaxInteger: number = 99_999_999; + const randomInteger: number = this.randomGenerator.getRandomInteger(rangeMinInteger, rangeMaxInteger); + const hexadecimalNumber: string = NumberUtils.toHex(randomInteger); + const prefixLength: number = Utils.hexadecimalPrefix.length; + const baseNameLength: number = + (nameLength ?? HexadecimalIdentifierNamesGenerator.baseIdentifierNameLength) + prefixLength; + const baseIdentifierName: string = hexadecimalNumber.slice(0, baseNameLength); + const identifierName: string = `_${baseIdentifierName}`; + + if (!validationFn(identifierName)) { + return this.generateNextName(nameLength, validationFn); + } + + this.preserveName(identifierName); + + return identifierName; + } } diff --git a/src/generators/identifier-names-generators/MangledIdentifierNamesGenerator.ts b/src/generators/identifier-names-generators/MangledIdentifierNamesGenerator.ts index c5ca35c87..96d6c3371 100644 --- a/src/generators/identifier-names-generators/MangledIdentifierNamesGenerator.ts +++ b/src/generators/identifier-names-generators/MangledIdentifierNamesGenerator.ts @@ -96,22 +96,15 @@ export class MangledIdentifierNamesGenerator extends AbstractIdentifierNamesGene * @returns {string} */ public generateForGlobalScope(nameLength?: number): string { - const prefix: string = this.options.identifiersPrefix ? `${this.options.identifiersPrefix}` : ''; - - const identifierName: string = this.generateNewMangledName( - this.lastMangledName, - (newIdentifierName: string) => { - const identifierNameWithPrefix: string = `${prefix}${newIdentifierName}`; - - return this.isValidIdentifierNameInAllScopes(identifierNameWithPrefix); - } - ); - const identifierNameWithPrefix: string = `${prefix}${identifierName}`; - - this.updatePreviousMangledName(identifierName); - this.preserveName(identifierNameWithPrefix); + return this.generateForGlobalScopeInternal((name) => this.isValidIdentifierName(name)); + } - return identifierNameWithPrefix; + /** + * @param {number} nameLength + * @returns {string} + */ + public generateForGlobalScopeWithAllScopesValidation(nameLength?: number): string { + return this.generateForGlobalScopeInternal((name) => this.isValidIdentifierNameInAllScopes(name)); } /** @@ -299,6 +292,29 @@ export class MangledIdentifierNamesGenerator extends AbstractIdentifierNamesGene return identifierName; } + /** + * @param {(name: string) => boolean} validationFn + * @returns {string} + */ + private generateForGlobalScopeInternal(validationFn: (name: string) => boolean): string { + const prefix: string = this.options.identifiersPrefix ? `${this.options.identifiersPrefix}` : ''; + + const identifierName: string = this.generateNewMangledName( + this.lastMangledName, + (newIdentifierName: string) => { + const identifierNameWithPrefix: string = `${prefix}${newIdentifierName}`; + + return validationFn(identifierNameWithPrefix); + } + ); + const identifierNameWithPrefix: string = `${prefix}${identifierName}`; + + this.updatePreviousMangledName(identifierName); + this.preserveName(identifierNameWithPrefix); + + return identifierNameWithPrefix; + } + /** * @param {TNodeWithLexicalScope[]} lexicalScopeNodes * @returns {string} diff --git a/src/interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator.ts b/src/interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator.ts index a5164d74a..11dad39eb 100644 --- a/src/interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator.ts +++ b/src/interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator.ts @@ -47,6 +47,18 @@ export interface IIdentifierNamesGenerator { */ isValidIdentifierNameInLexicalScopes(identifierName: string, lexicalScopeNodes: TNodeWithLexicalScope[]): boolean; + /** + * @param {string} identifierName + * @returns {boolean} + */ + isValidIdentifierNameInAllScopes(identifierName: string): boolean; + + /** + * @param {number} nameLength + * @returns {string} + */ + generateForGlobalScopeWithAllScopesValidation(nameLength?: number): string; + /** * @param {string} identifierName */ From 25427bcc0b8058de04fc9f434066a922e8bf5e38 Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Tue, 3 Feb 2026 20:22:11 +0400 Subject: [PATCH 17/43] Add Pro API support to CLI including support for large files obfuscation (#1381) --- CHANGELOG.md | 5 + README.md | 222 +++++- bin/javascript-obfuscator | 5 +- package.json | 3 +- src/JavaScriptObfuscatorCLIFacade.ts | 5 +- src/JavaScriptObfuscatorFacade.ts | 15 +- src/cli/JavaScriptObfuscatorCLI.ts | 215 +++++- src/cli/sanitizers/StrictModeSanitizer.ts | 13 + src/interfaces/options/ICLIOptions.ts | 2 + src/interfaces/pro-api/IProApiClient.ts | 8 + src/pro-api/ProApiClient.ts | 272 ++++++-- src/pro-api/enums/VMBytecodeFormat.ts | 9 + src/pro-api/enums/VMTargetFunctionsMode.ts | 9 + .../cli/JavaScriptObfuscatorCLI.spec.ts | 368 +++++++--- .../pro-api/ProApiClient.spec.ts | 652 +++++++++--------- test/index.spec.ts | 1 + test/unit-tests/pro-api/ProApiClient.spec.ts | 183 ++++- yarn.lock | 43 ++ 18 files changed, 1523 insertions(+), 507 deletions(-) create mode 100644 src/cli/sanitizers/StrictModeSanitizer.ts create mode 100644 src/pro-api/enums/VMBytecodeFormat.ts create mode 100644 src/pro-api/enums/VMTargetFunctionsMode.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 670232a2f..35118b467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ Change Log +v5.3.0 +--- +* Add Pro API support to CLI +* Add large files upload support to Pro API + v5.2.1 --- * Fixed `transformObjectKeys` incorrectly hoisting object literal outside of loop when loop body is a single statement without braces, causing all iterations to share the same object reference. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1300 diff --git a/README.md b/README.md index 128eaba72..fe9b9759a 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,20 @@ Huge thanks to all supporters! --- -### :rocket: JavaScript Obfuscator Pro with VM Obfuscation is out! +### :rocket: Obfuscator.io with VM Obfuscation is out! -**JavaScript Obfuscator Pro** features **VM-based bytecode obfuscation** β€” the most advanced code protection available. Your JavaScript functions are transformed into custom bytecode running on an embedded virtual machine, making reverse engineering extremely difficult. +**Obfuscator.io** features **VM-based bytecode obfuscation** β€” the most advanced code protection available. Your JavaScript functions are transformed into custom bytecode running on an embedded virtual machine, making reverse engineering extremely difficult. [Try it at obfuscator.io](https://obfuscator.io) +This package provides access to Obfuscator.io Pro API via CLI and Node.js API. + --- JavaScript Obfuscator is a powerful free obfuscator for JavaScript, containing a variety of features which provide protection for your source code. **Key features:** -- VM bytecode obfuscation (via [JavaScript Obfuscator Pro](https://obfuscator.io/)) +- VM bytecode obfuscation (via [Obfuscator.io](https://obfuscator.io/)) - variables renaming - strings extraction and encryption - dead code injection @@ -314,17 +316,17 @@ console.log(result.getObfuscatedCode()); **Parameters:** * `sourceCode` (`string`) – source code to obfuscate -* `options` (`Object`) – obfuscation options. **Must include `vmObfuscation: true`** +* `options` (`Object`) – obfuscation options. **Must include at least one Pro feature: `vmObfuscation: true` or `parseHtml: true`** * `apiConfig` (`Object`) – Pro API configuration: * `apiToken` (`string`, required) – your API token from obfuscator.io * `timeout` (`number`, optional) – request timeout in ms (default: `300000` - 5 minutes) - * `version` (`string`, optional) – JavaScript Obfuscator Pro version to use (e.g., `'5.0.0-beta.20'`). Defaults to latest version if not specified. + * `version` (`string`, optional) – Obfuscator.io version to use (e.g., `'5.0.3'`). Defaults to latest version if not specified. * `onProgress` (`function`, optional) – callback for progress updates during obfuscation **Returns:** `Promise` **Throws:** `ApiError` if: -- `vmObfuscation` is not enabled in options +- No Pro features (`vmObfuscation` or `parseHtml`) are enabled in options - API token is invalid or expired - API request fails @@ -341,7 +343,7 @@ const result = await JavaScriptObfuscator.obfuscatePro( }, { apiToken: 'your_javascript_obfuscator_pro_api_token', - version: '5.0.0-beta.20' // Use specific version + version: '5.0.3' // Use specific version } ); ``` @@ -367,6 +369,28 @@ const result = await JavaScriptObfuscator.obfuscatePro( ); ``` +### Checking for Pro Features + +Use `ProApiClient.hasProFeatures()` to check if options require the Pro API: + +```javascript +const { ProApiClient } = require('javascript-obfuscator'); + +const options = { vmObfuscation: true, compact: true }; + +if (ProApiClient.hasProFeatures(options)) { + // Use obfuscatePro() - requires API token + const result = await JavaScriptObfuscator.obfuscatePro(sourceCode, options, { apiToken }); +} else { + // Use regular obfuscate() - no API token needed + const result = JavaScriptObfuscator.obfuscate(sourceCode, options); +} +``` + +Pro features include: +- `vmObfuscation: true` – VM-based bytecode obfuscation +- `parseHtml: true` – HTML parsing with inline JavaScript obfuscation + ### Error Handling ```javascript @@ -383,6 +407,36 @@ try { } ``` +### CLI Usage with Pro API + +You can also use Pro API features directly from the CLI by providing your API token: + +```sh +javascript-obfuscator input.js --pro-api-token YOUR_API_TOKEN --vm-obfuscation true -o output.js +``` + +With a specific obfuscator version: + +```sh +javascript-obfuscator input.js --pro-api-token YOUR_API_TOKEN --pro-api-version 5.0.3 --vm-obfuscation true -o output.js +``` + +**CLI Options:** +- `--pro-api-token ` – Your API token from [obfuscator.io](https://obfuscator.io) +- `--pro-api-version ` – Obfuscator.io version to use (optional, defaults to latest) + +The CLI automatically detects when Pro features (`vmObfuscation` or `parseHtml`) are enabled and routes the request through the Pro API. + +### Large File Uploads + +For files larger than ~4MB, the Pro API uses client-side uploads to Vercel Blob storage. To enable this feature, install the optional `@vercel/blob` package: + +```sh +npm install @vercel/blob +``` + +Without this package, large file obfuscation will fail with an error message prompting you to install it. + --- ## CLI usage @@ -583,6 +637,37 @@ Following options are available for the JS Obfuscator: --target [browser, browser-no-eval, node] --transform-object-keys --unicode-escape-sequence + --pro-api-token + --pro-api-version + --vm-obfuscation + --vm-obfuscation-threshold + --vm-preprocess-identifiers + --vm-dynamic-opcodes + --vm-target-functions '' (comma separated) + --vm-exclude-functions '' (comma separated) + --vm-target-functions-mode [root, comment] + --vm-wrap-top-level-initializers + --vm-opcode-shuffle + --vm-bytecode-encoding + --vm-bytecode-array-encoding + --vm-bytecode-array-encoding-key + --vm-bytecode-array-encoding-key-getter + --vm-instruction-shuffle + --vm-jumps-encoding + --vm-decoy-opcodes + --vm-dead-code-injection + --vm-split-dispatcher + --vm-macro-ops + --vm-debug-protection + --vm-runtime-opcode-derivation + --vm-stateful-opcodes + --vm-stack-encoding + --vm-randomize-keys + --vm-indirect-dispatch + --vm-compact-dispatcher + --vm-bytecode-format [binary, json] + --parse-html + --strict-mode ``` @@ -1766,9 +1851,9 @@ The performance will be at a relatively normal level -## JavaScript Obfuscator Pro Options +## Obfuscator.io Pro Options -> :warning: **The following VM obfuscation/Pro options are available only via the [JavaScript Obfuscator Pro API](https://obfuscator.io/).** +> :warning: **The following VM obfuscation/Pro options are available only via the [Obfuscator.io Pro API](https://obfuscator.io/).** > > To use these options, you need a Pro API token from [obfuscator.io](https://obfuscator.io) and must call the `obfuscatePro()` method instead of `obfuscate()`. See the [Pro API Methods](#shield-pro-api-methods-vm-obfuscation) section for details. @@ -1785,6 +1870,8 @@ Type: `number` Default: `1` Controls what percentage of your root-level functions get VM protection. +> **Warning:** Values other than `1` may cause runtime bugs when VM-obfuscated and non-VM-obfuscated code share top-level variables. A value of `1` is strongly recommended. For selective function obfuscation, use `vmTargetFunctionsMode: 'comment'` with the `// javascript-obfuscator:vm` directive instead. + ### `vmPreprocessIdentifiers` Type: `boolean` Default: `true` @@ -1915,6 +2002,56 @@ Type: `boolean` Default: `false` Encodes the entire bytecode array as a single block. The array is decoded once at startup before execution begins. Use together with `vmBytecodeEncoding` for two layers of protection. +### `vmBytecodeArrayEncodingKey` +Type: `string` Default: `''` + +Custom encryption key for bytecode array encoding. When set, this key is used instead of the default environment-derived key. The key must be provided at runtime via `vmBytecodeArrayEncodingKeyGetter`. + +This option externalizes the encryption key - it's not embedded in the obfuscated code itself. While the key is still accessible at runtime (and thus not truly secret), this separation prevents static analysis tools from finding the key by examining the code alone. + +**Important:** The key must be available **synchronously** when the obfuscated code loads. Use synchronous storage like cookies, localStorage, sessionStorage, global variables, or DOM elements (e.g., server-injected meta tags). Async methods like `fetch()` cannot be used directly in the key getter expression. + +### `vmBytecodeArrayEncodingKeyGetter` +Type: `string` Default: `''` + +**Synchronous** JavaScript expression that **returns** the encryption key at runtime. This expression is evaluated when the obfuscated code loads, and must return the same key that was provided in `vmBytecodeArrayEncodingKey`. + +**The obfuscated code will only work when the key getter returns exactly the same key that was used during obfuscation.** If the keys don't match, decryption will fail and the code will produce garbage or errors. If the key getter returns `undefined`, `null`, or an empty string, the code will throw an error: "VM decryption key not available". + +**Important:** The key should NOT be defined in the same JavaScript file/script as the obfuscated code. Doing so defeats the purpose of key externalization, as static analysis could still find the key. Store the key in a separate source: server-set cookies, localStorage populated by another script, server-injected HTML meta tags, or a global variable set by a different script that loads before the obfuscated code. + +Examples: +```ts +// From cookie +vmBytecodeArrayEncodingKeyGetter: "document.cookie.match(/vmKey=([^;]+)/)?.[1]" + +// From localStorage +vmBytecodeArrayEncodingKeyGetter: "localStorage.getItem('vmKey')" + +// From global variable +vmBytecodeArrayEncodingKeyGetter: "window.__VM_KEY__" + +// From meta tag (server-injected) +vmBytecodeArrayEncodingKeyGetter: "document.querySelector('meta[name=\"vm-key\"]').content" + +// From nested object +vmBytecodeArrayEncodingKeyGetter: "window.config.encryption.key" +``` + +**Usage example:** +```ts +// Build time +JavaScriptObfuscator.obfuscate(code, { + vmObfuscation: true, + vmBytecodeArrayEncoding: true, + vmBytecodeArrayEncodingKey: 'mySecretKey123', + vmBytecodeArrayEncodingKeyGetter: 'window.__VM_KEY__' +}); + +// Runtime - key must be set before obfuscated code runs +window.__VM_KEY__ = 'mySecretKey123'; +``` + ### `vmJumpsEncoding` Type: `boolean` Default: `false` @@ -2001,6 +2138,17 @@ Encrypts values on the VM stack during execution. Values are encoded when pushed This option heavily affects performance. +### `vmInstructionShuffle` +Type: `boolean` Default: `false` + +Randomizes the bytecode instruction layout per function. Each function can have a different instruction array format: +- Layout 0: `[op, arg, op, arg, ...]` (interleaved - default) +- Layout 1: `[arg, op, arg, op, ...]` (swapped interleaved) +- Layout 2: `[op0, op1, ..., arg0, arg1, ...]` (opcodes first, then arguments) +- Layout 3: `[arg0, arg1, ..., op0, op1, ...]` (arguments first, then opcodes) + +This makes pattern recognition across functions harder during analysis. + ### `vmRandomizeKeys` Type: `boolean` Default: `false` @@ -2025,6 +2173,62 @@ Available values: * `true` - force strict mode treatment for all code, even without explicit `'use strict'` directive. Use this when your code will run in strict mode context (e.g., in ES modules, bundlers, or modern frameworks). * `false` - only explicit strict mode indicators (`'use strict'`, ES modules, class methods) are treated as strict. Parent scope inheritance still applies per JS spec. +### `parseHtml` +Type: `boolean` Default: `false` + +Enables obfuscation of JavaScript within HTML ` + + + + +`; + +JavaScriptObfuscator.obfuscate(html, { + parseHtml: true, + stringArray: true +}); + +// output: HTML with only the marked script obfuscated +``` + ## Frequently Asked Questions ### What javascript versions are supported? diff --git a/bin/javascript-obfuscator b/bin/javascript-obfuscator index 0946b4521..144f7b8ad 100755 --- a/bin/javascript-obfuscator +++ b/bin/javascript-obfuscator @@ -1,3 +1,6 @@ #!/usr/bin/env node -require('../dist/index.cli').obfuscate(process.argv); \ No newline at end of file +require('../dist/index.cli').obfuscate(process.argv).catch((error) => { + console.error(error.message); + process.exit(1); +}); \ No newline at end of file diff --git a/package.json b/package.json index 4baf28c3f..2519534eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.2.1", + "version": "5.3.0", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", @@ -23,6 +23,7 @@ "dependencies": { "@javascript-obfuscator/escodegen": "2.3.1", "@javascript-obfuscator/estraverse": "5.4.0", + "@vercel/blob": ">=0.23.0", "acorn": "8.15.0", "assert": "2.1.0", "chalk": "4.1.2", diff --git a/src/JavaScriptObfuscatorCLIFacade.ts b/src/JavaScriptObfuscatorCLIFacade.ts index 09a66604b..075f21be5 100644 --- a/src/JavaScriptObfuscatorCLIFacade.ts +++ b/src/JavaScriptObfuscatorCLIFacade.ts @@ -6,11 +6,12 @@ class JavaScriptObfuscatorCLIFacade { /** * @param {string[]} argv */ - public static obfuscate(argv: string[]): void { + public static async obfuscate(argv: string[]): Promise { const javaScriptObfuscatorCLI: JavaScriptObfuscatorCLI = new JavaScriptObfuscatorCLI(argv); javaScriptObfuscatorCLI.initialize(); - javaScriptObfuscatorCLI.run(); + + return javaScriptObfuscatorCLI.run(); } } diff --git a/src/JavaScriptObfuscatorFacade.ts b/src/JavaScriptObfuscatorFacade.ts index d5128d0ee..91356e7c3 100644 --- a/src/JavaScriptObfuscatorFacade.ts +++ b/src/JavaScriptObfuscatorFacade.ts @@ -11,12 +11,10 @@ import { IInversifyContainerFacade } from './interfaces/container/IInversifyCont import { IJavaScriptObfuscator } from './interfaces/IJavaScriptObfsucator'; import { IObfuscationResult } from './interfaces/source-code/IObfuscationResult'; import { IProApiConfig, IProObfuscationResult, TProApiProgressCallback } from './interfaces/pro-api/IProApiClient'; -import { ApiError } from './pro-api/ApiError'; import { InversifyContainerFacade } from './container/InversifyContainerFacade'; import { Options } from './options/Options'; import { Utils } from './utils/Utils'; -import { ProApiClient } from './pro-api/ProApiClient'; class JavaScriptObfuscatorFacade { /** @@ -94,6 +92,7 @@ class JavaScriptObfuscatorFacade { /** * Obfuscate code using the Pro API (obfuscator.io) * This method requires a valid API token from obfuscator.io and only works with VM obfuscation. + * Only available in Node.js environment. * * @param {string} sourceCode - Source code to obfuscate * @param {TInputOptions} inputOptions - Obfuscation options (must include vmObfuscation: true) @@ -108,13 +107,13 @@ class JavaScriptObfuscatorFacade { proApiConfig: IProApiConfig, onProgress?: TProApiProgressCallback ): Promise { - if (!inputOptions.vmObfuscation) { - throw new ApiError( - 'obfuscatePro method works only with VM obfuscation. Set vmObfuscation: true in options.', - 400 - ); + if (typeof window !== 'undefined') { + const { ApiError } = await import('./pro-api/ApiError'); + + throw new ApiError('obfuscatePro is only available in Node.js environment', 500); } + const { ProApiClient } = await import('./pro-api/ProApiClient'); const client = new ProApiClient(proApiConfig); return client.obfuscate(sourceCode, inputOptions, onProgress); @@ -123,4 +122,4 @@ class JavaScriptObfuscatorFacade { export { JavaScriptObfuscatorFacade as JavaScriptObfuscator }; export { ApiError } from './pro-api/ApiError'; -export type { IProApiConfig, TProApiProgressCallback } from './interfaces/pro-api/IProApiClient'; +export type { IProApiConfig, IProObfuscationResult, TProApiProgressCallback } from './interfaces/pro-api/IProApiClient'; diff --git a/src/cli/JavaScriptObfuscatorCLI.ts b/src/cli/JavaScriptObfuscatorCLI.ts index 9d905accd..99b16d53a 100644 --- a/src/cli/JavaScriptObfuscatorCLI.ts +++ b/src/cli/JavaScriptObfuscatorCLI.ts @@ -8,6 +8,8 @@ import { TInputOptions } from '../types/options/TInputOptions'; import { IFileData } from '../interfaces/cli/IFileData'; import { IInitializable } from '../interfaces/IInitializable'; import { IObfuscationResult } from '../interfaces/source-code/IObfuscationResult'; +import { ProApiClient } from '../pro-api/ProApiClient'; +import { IProObfuscationResult } from '../interfaces/pro-api/IProApiClient'; import { initializable } from '../decorators/Initializable'; @@ -34,6 +36,9 @@ import { Logger } from '../logger/Logger'; import { ObfuscatedCodeFileUtils } from './utils/ObfuscatedCodeFileUtils'; import { SourceCodeFileUtils } from './utils/SourceCodeFileUtils'; import { Utils } from '../utils/Utils'; +import { VMTargetFunctionsMode } from '../pro-api/enums/VMTargetFunctionsMode'; +import { VMBytecodeFormat } from '../pro-api/enums/VMBytecodeFormat'; +import { StrictModeSanitizer } from './sanitizers/StrictModeSanitizer'; export class JavaScriptObfuscatorCLI implements IInitializable { /** @@ -155,7 +160,7 @@ export class JavaScriptObfuscatorCLI implements IInitializable { ); } - public run(): void { + public async run(): Promise { const canShowHelp: boolean = !this.arguments.length || this.arguments.includes('--help'); if (canShowHelp) { @@ -166,7 +171,7 @@ export class JavaScriptObfuscatorCLI implements IInitializable { const sourceCodeData: IFileData[] = this.sourceCodeFileUtils.readSourceCode(); - this.processSourceCodeData(sourceCodeData); + await this.processSourceCodeData(sourceCodeData); } private configureCommands(): void { @@ -210,7 +215,7 @@ export class JavaScriptObfuscatorCLI implements IInitializable { ) .option( '--domain-lock-redirect-url ', - 'Allows the browser to be redirected to a passed URL if the source code isn\'t run on the domains specified by --domain-lock' + "Allows the browser to be redirected to a passed URL if the source code isn't run on the domains specified by --domain-lock" ) .option( '--exclude (comma separated, without whitespaces)', @@ -390,6 +395,153 @@ export class JavaScriptObfuscatorCLI implements IInitializable { 'Allows to enable/disable string conversion to unicode escape sequence', BooleanSanitizer ) + .option( + '--pro-api-token ', + 'API token for Pro obfuscation via obfuscator.io (enables VM obfuscation via cloud API)' + ) + .option('--pro-api-version ', 'Obfuscator version to use with Pro API (e.g., "5.0.0")') + .option( + '--vm-obfuscation ', + 'Enables VM-based bytecode obfuscation for functions', + BooleanSanitizer + ) + .option( + '--vm-obfuscation-threshold ', + 'The probability that VM obfuscation will be applied to a function (Default: 1, Min: 0, Max: 1)', + parseFloat + ) + .option( + '--vm-preprocess-identifiers ', + 'Preprocesses identifiers before VM transformation (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-dynamic-opcodes ', + 'Dynamically assembles VM dispatcher with shuffled case order and filters unused opcodes based on code analysis', + BooleanSanitizer + ) + .option( + '--vm-target-functions (comma separated, without whitespaces)', + 'List of specific function names to apply VM obfuscation to (comma separated)', + ArraySanitizer + ) + .option( + '--vm-exclude-functions (comma separated, without whitespaces)', + 'List of function names to exclude from VM obfuscation (comma separated)', + ArraySanitizer + ) + .option( + '--vm-target-functions-mode ', + 'Controls how functions are selected for VM obfuscation. ' + + `Values: ${CLIUtils.stringifyOptionAvailableValues(VMTargetFunctionsMode)}. ` + + `Default: ${VMTargetFunctionsMode.Root}` + ) + .option( + '--vm-wrap-top-level-initializers ', + 'Wraps top-level variable initializers in IIFEs so they can be VM-obfuscated (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-opcode-shuffle ', + 'Randomizes the numeric values assigned to each opcode (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-bytecode-encoding ', + 'Enables bytecode encryption with per-function keys (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-bytecode-array-encoding ', + 'Enables encrypted bytecode array with lazy decryption (Default: false)', + BooleanSanitizer + ) + .option('--vm-bytecode-array-encoding-key ', 'Custom static key for bytecode array encoding') + .option( + '--vm-bytecode-array-encoding-key-getter ', + 'Custom key getter function code for bytecode array encoding' + ) + .option( + '--vm-instruction-shuffle ', + 'Shuffles instruction order within basic blocks (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-jumps-encoding ', + 'Enables jump target encoding to prevent CFG reconstruction (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-decoy-opcodes ', + 'Enables insertion of decoy opcodes and dead instructions (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-dead-code-injection ', + 'Enables dead code injection with opaque predicates in bytecode (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-split-dispatcher ', + 'Splits the VM interpreter into multiple category-based dispatchers (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-macro-ops ', + 'Enables macro-op fusion to combine common instruction sequences (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-debug-protection ', + 'Enables anti-debugging measures with state corruption (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-runtime-opcode-derivation ', + 'Enables runtime opcode derivation from seeds instead of static mappings (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-stateful-opcodes ', + 'Enables position-based stateful opcode decoding to prevent pattern matching (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-stack-encoding ', + 'Enables stack value encoding to prevent stack inspection (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-randomize-keys ', + 'Randomizes bytecode property keys to prevent pattern matching (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-indirect-dispatch ', + 'Uses indirect dispatch via handler function table instead of switch statement (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-compact-dispatcher ', + 'Uses a single unified dispatcher for both sync and generator execution, reducing code size (Default: false)', + BooleanSanitizer + ) + .option( + '--vm-bytecode-format ', + 'Sets the bytecode storage format. ' + + `Values: ${CLIUtils.stringifyOptionAvailableValues(VMBytecodeFormat)}. ` + + `Default: ${VMBytecodeFormat.Binary}` + ) + .option( + '--parse-html ', + 'Enables obfuscation of JavaScript within HTML