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

v5.11.0
---
* `domainLockRedirectUrl` now accepts an empty string to stop execution without redirecting when the domain check fails. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1217

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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,7 @@ Type: `string[]` Default: `[]`

Allows to run the obfuscated source code only on specific domains and/or sub-domains. This makes really hard for someone to just copy and paste your source code and run it elsewhere.

If the source code isn't run on the domains specified by this option, the browser will be redirected to a passed to the [`domainLockRedirectUrl`](#domainlockredirecturl) option URL.
If the source code isn't run on the domains specified by this option, the browser will be redirected to a passed to the [`domainLockRedirectUrl`](#domainlockredirecturl) option URL. If `domainLockRedirectUrl` is set to an empty string, execution of the obfuscated code is interrupted instead of redirecting.

##### Multiple domains and sub-domains
It's possible to lock your code to more than one domain or sub-domain. For instance, to lock it so the code only runs on **www.example.com** add `www.example.com`. To make it work on the root domain including any sub-domains (`example.com`, `sub.example.com`), use `.example.com`.
Expand All @@ -1039,6 +1039,8 @@ Type: `string` Default: `about:blank`

Allows the browser to be redirected to a passed URL if the source code isn't run on the domains specified by [`domainLock`](#domainlock)

If set to an empty string, the browser is not redirected. Instead, execution of the obfuscated code is interrupted when the domain check fails.

### `exclude`
Type: `string[]` Default: `[]`

Expand Down
3 changes: 2 additions & 1 deletion src/cli/JavaScriptObfuscatorCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
)
.option(
'--domain-lock-redirect-url <string>',
"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. " +
'Pass an empty string to throw an error instead of redirecting'
)
.option(
'--exclude <list> (comma separated, without whitespaces)',
Expand Down
10 changes: 5 additions & 5 deletions src/custom-code-helpers/domain-lock/DomainLockCodeHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,20 @@ export class DomainLockCodeHelper extends AbstractCustomCodeHelper {
protected override getCodeHelperTemplate(): string {
const domainsString: string = this.options.domainLock.join(';');
const domainsLockRedirectUrl: string = this.options.domainLockRedirectUrl;
const domainLockRedirectUrlIsEmpty: boolean = !domainsLockRedirectUrl.length;
const [hiddenDomainsString, domainsStringDiff]: string[] = this.cryptUtils.hideString(
domainsString,
domainsString.length * 3
);
const [hiddenDomainLockRedirectUrl, domainLockRedirectUrlDiff]: string[] = this.cryptUtils.hideString(
domainsLockRedirectUrl,
domainsLockRedirectUrl.length * 3
);
const [hiddenDomainLockRedirectUrl, domainLockRedirectUrlDiff]: string[] = domainLockRedirectUrlIsEmpty
? ['', '']
: this.cryptUtils.hideString(domainsLockRedirectUrl, domainsLockRedirectUrl.length * 3);
const globalVariableTemplate: string =
this.options.target !== ObfuscationTarget.BrowserNoEval
? this.getGlobalVariableTemplate()
: GlobalVariableNoEvalTemplate();

return this.customCodeHelperFormatter.formatTemplate(DomainLockTemplate(), {
return this.customCodeHelperFormatter.formatTemplate(DomainLockTemplate(domainLockRedirectUrlIsEmpty), {
callControllerFunctionName: this.callsControllerFunctionName,
domainLockFunctionName: this.domainLockFunctionName,
domainsStringDiff,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
/**
* @param {boolean} domainLockRedirectUrlIsEmpty
* @returns {string}
*/
export function DomainLockTemplate (): string {
export function DomainLockTemplate (domainLockRedirectUrlIsEmpty: boolean): string {
const domainLockRedirectUrlBlock: string = domainLockRedirectUrlIsEmpty
? 'throw new Error();'
: `const regExp2 = new RegExp("[{domainLockRedirectUrlDiff}]", "g");
const domainLockRedirectUrl = "{hiddenDomainLockRedirectUrl}".replace(regExp2, "");

that[document][location] = domainLockRedirectUrl;`;

return `
const {domainLockFunctionName} = {callControllerFunctionName}(this, function () {

Expand Down Expand Up @@ -107,10 +115,7 @@ export function DomainLockTemplate (): string {
}

if (!ok) {
const regExp2 = new RegExp("[{domainLockRedirectUrlDiff}]", "g");
const domainLockRedirectUrl = "{hiddenDomainLockRedirectUrl}".replace(regExp2, "");

that[document][location] = domainLockRedirectUrl;
${domainLockRedirectUrlBlock}
}
});

Expand Down
20 changes: 16 additions & 4 deletions src/options/validators/IsDomainLockRedirectUrl.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsUrl, ValidateIf } from 'class-validator';
import { ValidateBy, ValidateIf, buildMessage, isURL } from 'class-validator';

import { TInputOptions } from '../../types/options/TInputOptions';

Expand All @@ -18,9 +18,21 @@ export const IsDomainLockRedirectUrl = (): PropertyDecorator => {
ValidateIf(({ domainLockRedirectUrl }: TInputOptions) => {
return domainLockRedirectUrl !== DEFAULT_PRESET.domainLockRedirectUrl;
})(target, key);
IsUrl({
require_protocol: false,
require_host: false
// an empty string is checked here instead of `ValidateIf`,
// so it's still validated with `IsAllowedForObfuscationTargets`
ValidateBy({
name: 'isDomainLockRedirectUrl',
validator: {
validate: (value: unknown): boolean =>
value === '' ||
isURL(<string>value, {
require_protocol: false,
require_host: false
}),
defaultMessage: buildMessage(
(eachPrefix: string) => `${eachPrefix}$property must be a URL address or an empty string`
)
}
})(target, key);
IsAllowedForObfuscationTargets([ObfuscationTarget.Browser, ObfuscationTarget.BrowserNoEval])(
target,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ import { readFileAsString } from '../../../../helpers/readFileAsString';
function getFunctionFromTemplate(
templateData: any,
callsControllerFunctionName: string,
documentTemplate: string
documentTemplate: string,
domainLockRedirectUrlIsEmpty: boolean = false
): Function {
const domainLockTemplate: string = format(DomainLockTemplate(), templateData);
const domainLockTemplate: string = format(DomainLockTemplate(domainLockRedirectUrlIsEmpty), templateData);

return Function(`
${documentTemplate}
Expand Down Expand Up @@ -1011,4 +1012,90 @@ describe('DomainLockTemplate', () => {
assert.doesNotThrow(testFunc);
});
});

describe('Variant #11: empty `domainLockRedirectUrl`', () => {
const domainsString: string = ['www.example.com'].join(';');

describe('Variant #1: current domain matches with `domainsString`', () => {
const currentDomain: string = 'www.example.com';

let testFunc: Function;
let root: any;

before(() => {
const [hiddenDomainsString, domainsStringDiff] = cryptUtils.hideString(
domainsString,
domainsString.length * 3
);

root = {
document: {
domain: currentDomain,
location: undefined
}
};

testFunc = getFunctionFromTemplate(
{
domainLockFunctionName: 'domainLockFunction',
domainsStringDiff,
domains: hiddenDomainsString,
domainLockRedirectUrlDiff: '',
hiddenDomainLockRedirectUrl: '',
globalVariableTemplate: '',
singleCallControllerFunctionName
},
singleCallControllerFunctionName,
thatThisTemplate,
true
);
});

it('should not throw and should not change document.location', () => {
assert.doesNotThrow(() => testFunc.apply(root));
assert.isUndefined(root.document.location);
});
});

describe("Variant #2: current domain doesn't match with `domainsString`", () => {
const currentDomain: string = 'www.test.com';

let testFunc: Function;
let root: any;

before(() => {
const [hiddenDomainsString, domainsStringDiff] = cryptUtils.hideString(
domainsString,
domainsString.length * 3
);

root = {
document: {
domain: currentDomain,
location: undefined
}
};

testFunc = getFunctionFromTemplate(
{
domainLockFunctionName: 'domainLockFunction',
domainsStringDiff,
domains: hiddenDomainsString,
domainLockRedirectUrlDiff: '',
hiddenDomainLockRedirectUrl: '',
globalVariableTemplate: '',
singleCallControllerFunctionName
},
singleCallControllerFunctionName,
thatThisTemplate,
true
);
});

it('should throw and should not change document.location', () => {
assert.throws(() => testFunc.apply(root));
assert.isUndefined(root.document.location);
});
});
});
});
40 changes: 40 additions & 0 deletions test/functional-tests/options/OptionsNormalizer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,46 @@ describe('OptionsNormalizer', () => {
assert.deepEqual(optionsPreset, expectedOptionsPreset);
});
});

describe('Variant #3: `domainLock` option is set and redirect URL is empty', () => {
before(() => {
optionsPreset = getNormalizedOptions({
...getDefaultOptions(),
domainLock: ['localhost'],
domainLockRedirectUrl: ''
});

expectedOptionsPreset = {
...getDefaultOptions(),
domainLock: ['localhost'],
domainLockRedirectUrl: ''
};
});

it('should not normalize options preset', () => {
assert.deepEqual(optionsPreset, expectedOptionsPreset);
});
});

describe('Variant #4: `domainLock` option is not set and redirect URL is empty', () => {
before(() => {
optionsPreset = getNormalizedOptions({
...getDefaultOptions(),
domainLock: [],
domainLockRedirectUrl: ''
});

expectedOptionsPreset = {
...getDefaultOptions(),
domainLock: [],
domainLockRedirectUrl: 'about:blank'
};
});

it('should normalize options preset', () => {
assert.deepEqual(optionsPreset, expectedOptionsPreset);
});
});
});

describe('domainLockRule', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { assert } from 'chai';

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

import { ObfuscationTarget } from '../../../../src/enums/ObfuscationTarget';

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

describe('`domainLockRedirectUrl` validation', () => {
Expand Down Expand Up @@ -70,6 +72,23 @@ describe('`domainLockRedirectUrl` validation', () => {
assert.doesNotThrow(testFunc);
});
});

describe('Variant #5: empty string', () => {
let testFunc: () => string;

beforeEach(() => {
testFunc = () =>
JavaScriptObfuscator.obfuscate('', {
...NO_ADDITIONAL_NODES_PRESET,
domainLock: ['example.com'],
domainLockRedirectUrl: ''
}).getObfuscatedCode();
});

it('should pass validation', () => {
assert.doesNotThrow(testFunc);
});
});
});

describe('Variant #2: negative validation', () => {
Expand All @@ -89,6 +108,24 @@ describe('`domainLockRedirectUrl` validation', () => {
assert.throws(testFunc, expectedError);
});
});

describe('Variant #2: empty string with `node` obfuscation target', () => {
const expectedError: string = 'This option allowed only for obfuscation targets';
let testFunc: () => string;

beforeEach(() => {
testFunc = () =>
JavaScriptObfuscator.obfuscate('', {
...NO_ADDITIONAL_NODES_PRESET,
domainLockRedirectUrl: '',
target: ObfuscationTarget.Node
}).getObfuscatedCode();
});

it('should not pass validation', () => {
assert.throws(testFunc, expectedError);
});
});
});
});
});