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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
Change Log

v5.10.1
---
* Fixed `controlFlowFlattening` changing `let` and `const` declarations into `var` inside functions passed as call arguments or used as operands, when `var` is the prevailing kind of variables. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1471
* Fixed `controlFlowFlattening` turning a `#x in obj` private brand check into invalid code. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1472
* Fixed an escaped `'use\x20strict'` directive being emitted as an actual `'use strict'` directive. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1474
* Fixed `selfDefending`, `disableConsoleOutput`, `debugProtection` and `domainLock` code helpers being placed before the `'use strict'` directive, which disabled strict mode. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1475
* Fixed `numbersToExpressions` producing invalid code for non-computed numeric keys of class members. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1476
* Template literals are now transformed into `String.prototype.concat` calls instead of `+` concatenation, so substitutions are converted to strings with `toString` instead of `valueOf`. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1477

v5.10.0
---
* Replaced `multimatch` with `minimatch@11` for the CLI `--exclude` option, resolving the `minimatch` denial of service advisory `GHSA-g3hv-75wg-m249`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "javascript-obfuscator",
"version": "5.10.0",
"version": "5.10.1",
"description": "JavaScript obfuscator",
"keywords": [
"obfuscator",
Expand Down
12 changes: 9 additions & 3 deletions src/custom-code-helpers/CustomCodeHelperFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ export class CustomCodeHelperFormatter implements ICustomCodeHelperFormatter {
return;
}

if (this.prevailingKindOfVariables === 'var') {
node.kind = 'var';
}
node.kind = this.formatVariableDeclarationKind(node.kind);

return node;
}
Expand All @@ -59,4 +57,12 @@ export class CustomCodeHelperFormatter implements ICustomCodeHelperFormatter {

return statements;
}

/**
* @param {VariableDeclaration['kind']} kind
* @returns {VariableDeclaration['kind']}
*/
public formatVariableDeclarationKind(kind: ESTree.VariableDeclaration['kind']): ESTree.VariableDeclaration['kind'] {
return this.prevailingKindOfVariables === 'var' ? 'var' : kind;
}
}
5 changes: 4 additions & 1 deletion src/custom-nodes/AbstractCustomNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ export abstract class AbstractCustomNode<TInitialData extends unknown[] = unknow
*/
public getNode(): TStatement[] {
if (!this.cachedNode) {
this.cachedNode = this.customCodeHelperFormatter.formatStructure(this.getNodeStructure());
// The structure isn't formatted with `formatStructure` as a whole, because custom nodes may embed nodes
// of the original code, and their variable declarations must keep their kind.
// Custom nodes set the kind of their own variable declarations with `formatVariableDeclarationKind`.
this.cachedNode = this.getNodeStructure();
}

return this.cachedNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class BlockStatementControlFlowFlatteningNode extends AbstractCustomNode
)
)
],
'const'
this.customCodeHelperFormatter.formatVariableDeclarationKind('const')
),
NodeFactory.variableDeclarationNode(
[
Expand All @@ -103,7 +103,7 @@ export class BlockStatementControlFlowFlatteningNode extends AbstractCustomNode
NodeFactory.literalNode(0)
)
],
'let'
this.customCodeHelperFormatter.formatVariableDeclarationKind('let')
),
NodeFactory.whileStatementNode(
NodeFactory.literalNode(true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class ControlFlowStorageNode extends AbstractCustomNode {
NodeFactory.objectExpressionNode(propertyNodes)
)
],
'const'
this.customCodeHelperFormatter.formatVariableDeclarationKind('const')
);

return [structure];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class ObjectExpressionVariableDeclarationHostNode extends AbstractCustomN
NodeFactory.objectExpressionNode(this.properties)
)
],
'const'
this.customCodeHelperFormatter.formatVariableDeclarationKind('const')
);

return [structure];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class StringArrayScopeCallsWrapperVariableNode extends AbstractStringArra
NodeFactory.identifierNode(this.stringArrayCallsWrapperData.name)
)
],
'const'
this.customCodeHelperFormatter.formatVariableDeclarationKind('const')
);

NodeUtils.parentizeAst(structure);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as ESTree from 'estree';

import { TDictionary } from '../../types/TDictionary';
import { TStatement } from '../../types/node/TStatement';

Expand All @@ -14,4 +16,10 @@ export interface ICustomCodeHelperFormatter {
* @returns {TStatement[]}
*/
formatStructure(structure: TStatement[]): TStatement[];

/**
* @param {VariableDeclaration['kind']} kind
* @returns {VariableDeclaration['kind']}
*/
formatVariableDeclarationKind(kind: ESTree.VariableDeclaration['kind']): ESTree.VariableDeclaration['kind'];
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ControlFlowCustomNode } from '../../../enums/custom-nodes/ControlFlowCu

import { BinaryExpressionFunctionNode } from '../../../custom-nodes/control-flow-flattening-nodes/BinaryExpressionFunctionNode';
import { ExpressionWithOperatorControlFlowReplacer } from './ExpressionWithOperatorControlFlowReplacer';
import { NodeGuards } from '../../../node/NodeGuards';

@injectFromBase()
@injectable()
Expand Down Expand Up @@ -54,6 +55,10 @@ export class BinaryExpressionControlFlowReplacer extends ExpressionWithOperatorC
parentNode: ESTree.Node,
controlFlowStorage: IControlFlowStorage
): ESTree.Node {
if (NodeGuards.isPrivateIdentifierNode(binaryExpressionNode.left)) {
return binaryExpressionNode;
}

const operator: ESTree.BinaryOperator = binaryExpressionNode.operator;
const binaryExpressionFunctionCustomNode: ICustomNode<TInitialData<BinaryExpressionFunctionNode>> =
this.controlFlowCustomNodeFactory(ControlFlowCustomNode.BinaryExpressionFunctionNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ export class NumberToNumericalExpressionTransformer extends AbstractNodeTransfor
return literalNode;
}

if (
(NodeGuards.isMethodDefinitionNode(parentNode) || NodeGuards.isPropertyDefinitionNode(parentNode)) &&
!parentNode.computed &&
parentNode.key === literalNode
) {
return literalNode;
}

const baseNumber: number = literalNode.value;
const [integerPart, decimalPart] = NumberUtils.extractIntegerAndDecimalParts(baseNumber);
const integerNumberNumericalExpressionData: TNumberNumericalExpressionData =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,43 @@ export class TemplateLiteralTransformer extends AbstractNodeTransformer {
return !!node && NodeGuards.isLiteralNode(node) && typeof node.value === 'string';
}

/**
* Unlike `+`, that converts substitutions with the `default` hint of `ToPrimitive` and prefers `valueOf`,
* `String.prototype.concat` converts them with `ToString`, as template literals do.
*
* Each `concat` call gets at most one non-literal substitution, so each substitution is converted
* before the next one is evaluated, as in template literals.
*
* @param {Expression[]} nodes
* @returns {CallExpression}
*/
private static buildConcatCallExpressionNode(nodes: ESTree.Expression[]): ESTree.CallExpression {
let callExpressionNode: ESTree.CallExpression | null = null;
let callExpressionHasNonLiteralArgument: boolean = false;

const nodesLength: number = nodes.length;

for (let i: number = 1; i < nodesLength; i++) {
const node: ESTree.Expression = nodes[i];
const isLiteralNode: boolean = NodeGuards.isLiteralNode(node);

if (callExpressionNode && (isLiteralNode || !callExpressionHasNonLiteralArgument)) {
callExpressionNode.arguments.push(node);
callExpressionHasNonLiteralArgument = callExpressionHasNonLiteralArgument || !isLiteralNode;

continue;
}

callExpressionNode = NodeFactory.callExpressionNode(
NodeFactory.memberExpressionNode(callExpressionNode ?? nodes[0], NodeFactory.identifierNode('concat')),
[node]
);
callExpressionHasNonLiteralArgument = !isLiteralNode;
}

return <ESTree.CallExpression>callExpressionNode;
}

/**
* @param {NodeTransformationStage} nodeTransformationStage
* @returns {IVisitor | null}
Expand Down Expand Up @@ -111,34 +148,15 @@ export class TemplateLiteralTransformer extends AbstractNodeTransformer {
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(filteredNodes[0]) &&
!TemplateLiteralTransformer.isLiteralNodeWithStringValue(filteredNodes[1])
) {
// ensure the first node is a string, so `concat` can be called on it
if (!TemplateLiteralTransformer.isLiteralNodeWithStringValue(filteredNodes[0])) {
filteredNodes.unshift(NodeFactory.literalNode(''));
}

let transformedNode: ESTree.Node;

if (filteredNodes.length > 1) {
let root: ESTree.BinaryExpression = NodeFactory.binaryExpressionNode(
'+',
<ESTree.Literal>filteredNodes[0],
filteredNodes[1]
);

// 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 = filteredNodes[0];
}
const transformedNode: ESTree.Expression =
filteredNodes.length > 1
? TemplateLiteralTransformer.buildConcatCallExpressionNode(filteredNodes)
: filteredNodes[0];

NodeUtils.parentizeAst(transformedNode);
NodeUtils.parentizeNode(transformedNode, parentNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import { NodeUtils } from '../../node/NodeUtils';
/**
* It's easier to fix "use strict"; placement after obfuscation as a separate stage
* than ignore this directive in other transformers like control flow and dead code injection transformers
*
* Directives are collected after custom code helpers are appended at the preparing stage,
* so they are looked up across the whole scope body rather than at its top.
* It's safe, because the parser marks as directives only statements of the directive prologue
*/
@injectFromBase()
@injectable()
Expand All @@ -32,11 +36,11 @@ export class DirectivePlacementTransformer extends AbstractNodeTransformer {
public override readonly runAfter: NodeTransformer[] = [NodeTransformer.CustomCodeHelpersTransformer];

/**
* @type {WeakMap<TNodeWithLexicalScope, Directive>}
* @type {WeakMap<TNodeWithLexicalScope, Directive[]>}
*/
private readonly lexicalScopeDirectives: WeakMap<TNodeWithLexicalScope, ESTree.Directive> = new WeakMap<
private readonly lexicalScopeDirectives: WeakMap<TNodeWithLexicalScope, ESTree.Directive[]> = new WeakMap<
TNodeWithLexicalScope,
ESTree.Directive
ESTree.Directive[]
>();

/**
Expand Down Expand Up @@ -98,10 +102,16 @@ export class DirectivePlacementTransformer extends AbstractNodeTransformer {
return nodeWithLexicalScopeStatements;
}

const firstStatementNode = nodeWithLexicalScopeStatements.body[0] ?? null;
const directiveNodes: ESTree.Directive[] = [];

if (firstStatementNode && NodeGuards.isDirectiveNode(firstStatementNode)) {
this.lexicalScopeDirectives.set(parentNode, firstStatementNode);
for (const statementNode of nodeWithLexicalScopeStatements.body) {
if (NodeGuards.isDirectiveNode(statementNode)) {
directiveNodes.push(statementNode);
}
}

if (directiveNodes.length) {
this.lexicalScopeDirectives.set(parentNode, directiveNodes);
}

return nodeWithLexicalScopeStatements;
Expand All @@ -120,17 +130,27 @@ export class DirectivePlacementTransformer extends AbstractNodeTransformer {
return nodeWithLexicalScopeStatements;
}

const directiveNode: ESTree.Directive | undefined = this.lexicalScopeDirectives.get(parentNode);
const directiveNodes: ESTree.Directive[] | undefined = this.lexicalScopeDirectives.get(parentNode);

if (directiveNode) {
const newDirectiveNode: ESTree.Directive = NodeUtils.clone(directiveNode);
if (directiveNodes) {
const newDirectiveNodes: ESTree.Directive[] = directiveNodes.map((directiveNode: ESTree.Directive) => {
const newDirectiveNode: ESTree.Directive = NodeUtils.clone(directiveNode);

// append new directive node at the top of lexical scope statements
NodeAppender.prepend(nodeWithLexicalScopeStatements, [newDirectiveNode]);
// the code generator prints the cooked value of a literal,
// so `'use\x20strict'` would become an actual `'use strict'` directive
if (newDirectiveNode.expression.raw !== undefined) {
NodeUtils.addXVerbatimPropertyTo(newDirectiveNode.expression);
}

return newDirectiveNode;
});

nodeWithLexicalScopeStatements.body = nodeWithLexicalScopeStatements.body.filter(
(node) => node !== directiveNode
(node: ESTree.Statement | ESTree.ModuleDeclaration) =>
!directiveNodes.includes(<ESTree.Directive>node)
);

NodeAppender.prepend(nodeWithLexicalScopeStatements, newDirectiveNodes);
}

return nodeWithLexicalScopeStatements;
Expand Down
Loading
Loading