Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
Change Log

v5.4.7
---
* Fixed directory obfuscation with a set `sourceMapFileName` making all files share and overwrite one `.map`. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/817
* Fixed CLI `--config` failures hiding the real cause behind a generic `Cannot open config file` message. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1101
* Fixed `sourceMapFileName` ending in `.js.map` (e.g. `foo.min.js.map`) being mangled in the emitted `//# sourceMappingURL=` comment. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1312
* Fixed `URIError: URI malformed` crash when `stringArray` with `base64`/`rc4` encoding processed a string literal containing lone surrogate code units (e.g. `"[^\uD800-\uDFFF]"`). Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1431
* Bumped the production `brace-expansion` transitive dependency to a patched version, resolving `CVE-2026-25547`. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1405

v5.4.6
---
* Fixed unicode (`\uXXXX`, `\u{XXXX}`) and hex (`\xXX`) escape sequences of string literals being un-escaped into their literal characters during obfuscation. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/345
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "javascript-obfuscator",
"version": "5.4.6",
"version": "5.4.7",
"description": "JavaScript obfuscator",
"keywords": [
"obfuscator",
Expand Down Expand Up @@ -100,6 +100,9 @@
"webpack-cli": "6.0.1",
"webpack-node-externals": "3.0.0"
},
"resolutions": {
"multimatch/minimatch/brace-expansion": "^1.1.12"
},
"repository": {
"type": "git",
"url": "git+https://github.com/javascript-obfuscator/javascript-obfuscator.git"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
import * as estraverse from '@javascript-obfuscator/estraverse';
import * as ESTree from 'estree';

import { TStringArrayEncoding } from '../../types/options/TStringArrayEncoding';
import { TStringLiteralNode } from '../../types/node/TStringLiteralNode';

import { IOptions } from '../../interfaces/options/IOptions';
Expand All @@ -12,6 +13,8 @@ import { IStringArrayStorage } from '../../interfaces/storages/string-array-tran
import { IStringArrayStorageAnalyzer } from '../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-transformers/IStringArrayStorageItem';

import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding';

import { NodeGuards } from '../../node/NodeGuards';
import { NodeLiteralUtils } from '../../node/NodeLiteralUtils';
import { NodeMetadata } from '../../node/NodeMetadata';
Expand All @@ -26,6 +29,14 @@ export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer {
*/
private static readonly minimumLengthForStringArray: number = 3;

/**
* Matches a lone (unpaired) surrogate code unit. Because of the `u` flag, valid surrogate pairs are
* iterated as a single code point outside the `\uD800-\uDFFF` range, so only unpaired surrogates match.
*
* @type {RegExp}
*/
private static readonly loneSurrogateRegExp: RegExp = /[\uD800-\uDFFF]/u;

/**
* @type {IOptions}
*/
Expand Down Expand Up @@ -128,6 +139,14 @@ export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer {
* @returns {boolean}
*/
private shouldAddValueToStringArray(literalNode: TStringLiteralNode): boolean {
// `base64` and `rc4` encodings rely on `encodeURIComponent`/`decodeURIComponent`, which cannot
// represent lone (unpaired) surrogate code units. Keeping such values inline avoids a
// `URIError: URI malformed` crash while still producing valid obfuscated code.
// Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1431
if (this.isProhibitedStringArrayValue(literalNode.value)) {
return false;
}

const isForceTransformNode: boolean = NodeMetadata.isForceTransformNode(literalNode);

if (isForceTransformNode) {
Expand All @@ -140,4 +159,17 @@ export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer {
this.randomGenerator.getMathRandom() <= this.options.stringArrayThreshold
);
}

/**
* @param {string} value
* @returns {boolean}
*/
private isProhibitedStringArrayValue(value: string): boolean {
const hasUnicodeEncoding: boolean = this.options.stringArrayEncoding.some(
(encoding: TStringArrayEncoding): boolean =>
encoding === StringArrayEncoding.Base64 || encoding === StringArrayEncoding.Rc4
);

return hasUnicodeEncoding && StringArrayStorageAnalyzer.loneSurrogateRegExp.test(value);
}
}
44 changes: 33 additions & 11 deletions src/cli/utils/CLIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,32 @@ export class CLIUtils {
* @returns {TDictionary}
*/
public static getUserConfig(configPath: string): TDictionary {
let config: TDictionary;

const configFileExtension: string = path.extname(configPath);
const isValidExtension: boolean = CLIUtils.allowedConfigFileExtensions.includes(configFileExtension);

if (!isValidExtension) {
throw new ReferenceError('Given config path must be a valid `.js|.mjs|.cjs` or `.json` file path');
throw new ReferenceError('Given config path must be a valid `.js`, `.cjs` or `.json` file path');
}

const errors: Error[] = [];

try {
config = require(configPath);
} catch {
try {
config = __non_webpack_require__(configPath);
} catch {
throw new ReferenceError(`Cannot open config file with path: ${configPath}`);
}
return require(configPath);
} catch (error) {
errors.push(<Error>error);
}

return config;
try {
return __non_webpack_require__(configPath);
} catch (error) {
errors.push(<Error>error);
}

// surface the underlying reason (invalid JSON, `ERR_REQUIRE_ESM`, missing file, ...)
// instead of masking it behind a generic message
throw new ReferenceError(
`Cannot open config file with path: ${configPath}. Reason: ${CLIUtils.getConfigErrorReason(errors)}`
);
}

/**
Expand All @@ -44,4 +50,20 @@ export class CLIUtils {
public static stringifyOptionAvailableValues(optionEnum: TDictionary): string {
return Object.values(optionEnum).join(`${StringSeparator.Comma} `);
}

/**
* @param {Error[]} errors
* @returns {string}
*/
private static getConfigErrorReason(errors: Error[]): string {
const [firstError, secondError] = errors;

// in the webpack bundle the real reason comes from the `__non_webpack_require__` call (the second error);
// in a plain Node/ts-node context `__non_webpack_require__` is not defined, so the real reason is the first error
if (secondError && !secondError.message.includes('__non_webpack_require__')) {
return secondError.message;
}

return firstError.message;
}
}
35 changes: 35 additions & 0 deletions src/cli/utils/ObfuscatedCodeFileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export class ObfuscatedCodeFileUtils {
throw new Error('Output code path is empty');
}

sourceMapFileName = this.getUniqueSourceMapFileName(outputCodePath, sourceMapFileName);

let normalizedOutputCodePath: string = path.normalize(outputCodePath);
let parsedOutputCodePath: path.ParsedPath = path.parse(normalizedOutputCodePath);

Expand Down Expand Up @@ -140,4 +142,37 @@ export class ObfuscatedCodeFileUtils {
encoding: JavaScriptObfuscatorCLI.encoding
});
}

/**
* For directory obfuscation a single `sourceMapFileName` would be shared by every file and the
* source maps would overwrite each other, so it is prefixed with the output code file name to
* keep each source map unique.
* https://github.com/javascript-obfuscator/javascript-obfuscator/issues/817
*
* @param {string} outputCodePath
* @param {string} sourceMapFileName
* @returns {string}
*/
private getUniqueSourceMapFileName(outputCodePath: string, sourceMapFileName: string): string {
if (!sourceMapFileName || !this.isDirectoryInputPath()) {
return sourceMapFileName;
}

const outputCodeName: string = path.parse(outputCodePath).name;
const parsedSourceMapFileName: path.ParsedPath = path.parse(sourceMapFileName);

// keep any leading directory part of `sourceMapFileName`, prefix only its file name
return path.join(parsedSourceMapFileName.dir, `${outputCodeName}-${parsedSourceMapFileName.base}`);
}

/**
* @returns {boolean}
*/
private isDirectoryInputPath(): boolean {
try {
return fs.lstatSync(this.inputPath).isDirectory();
} catch {
return false;
}
}
}
14 changes: 13 additions & 1 deletion src/options/normalizer-rules/SourceMapFileNameRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,19 @@ export const SourceMapFileNameRule: TOptionsNormalizerRule = (options: IOptions)
let { sourceMapFileName }: { sourceMapFileName: string } = options;

if (sourceMapFileName) {
sourceMapFileName = sourceMapFileName.replace(/^\/+/, '').replace(/(?:\.js)?(?:\.map)?$/, '');
sourceMapFileName = sourceMapFileName.replace(/^\/+/, '');

// a fully-qualified `*.js.map` file name is already in the canonical form, so it is kept as-is
// (otherwise the extension-stripping heuristic below would mangle names like `foo.min.js.map`)
// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1312
if (sourceMapFileName.endsWith('.js.map')) {
return {
...options,
sourceMapFileName
};
}

sourceMapFileName = sourceMapFileName.replace(/(?:\.js)?(?:\.map)?$/, '');

let sourceMapFileNameParts: string[] = sourceMapFileName.split(StringSeparator.Dot);
const sourceMapFileNamePartsCount: number = sourceMapFileNameParts.length;
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/invalid-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
compact: true,
selfDefending: false,
sourceMap: true
}
1 change: 1 addition & 0 deletions test/functional-tests/issues/fixtures/issue1431.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const controlSymbolRegexString = "\\\\[^\uD800-\uDFFF]";
52 changes: 52 additions & 0 deletions test/functional-tests/issues/issue1312.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { assert } from 'chai';

import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes';

import { SourceMapMode } from '../../../src/enums/source-map/SourceMapMode';

import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade';

//
// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1312
//
describe('Issue #1312', () => {
describe('`sourceMappingURL` should use the provided `sourceMapFileName`', () => {
describe('Variant #1: `*.min.js.map` file name', () => {
const sourceMappingUrlRegExp: RegExp = /\/\/# sourceMappingURL=a\.min\.js\.map$/;

let obfuscatedCode: string;

before(() => {
obfuscatedCode = JavaScriptObfuscator.obfuscate("console.log('Hello World');", {
...NO_ADDITIONAL_NODES_PRESET,
sourceMap: true,
sourceMapMode: SourceMapMode.Separate,
sourceMapFileName: 'a.min.js.map'
}).getObfuscatedCode();
});

it('should keep the full `.js.map` file name in the `sourceMappingURL`', () => {
assert.match(obfuscatedCode, sourceMappingUrlRegExp);
});
});

describe('Variant #2: base name without extension still gets `.js.map`', () => {
const sourceMappingUrlRegExp: RegExp = /\/\/# sourceMappingURL=a\.js\.map$/;

let obfuscatedCode: string;

before(() => {
obfuscatedCode = JavaScriptObfuscator.obfuscate("console.log('Hello World');", {
...NO_ADDITIONAL_NODES_PRESET,
sourceMap: true,
sourceMapMode: SourceMapMode.Separate,
sourceMapFileName: 'a'
}).getObfuscatedCode();
});

it('should append `.js.map` to a bare file name', () => {
assert.match(obfuscatedCode, sourceMappingUrlRegExp);
});
});
});
});
Loading
Loading