Configuration file format
The JSDefender configuration file is JSON formatted. By default, JSDefender looks for a file named jsdefender.config.json
in the current working directory.
This JSON code snippet shows the overall schema of the configuration file:
{
// Specifies the input files and their protection
"inputs": [],
// Specifies the source directory from which to get the input files
"sourceDir": "<mySourceDir>",
// Protected files go to this folder
"outDir": "<myOutput>",
// Specifies the map file, or turns it off
"mapFile": "<my-map.json> or 'off'",
// JSDefender Runtime goes to this file
"runtimeFile": "<myRuntime>",
// Specifies the way JSDefender Runtime is injected
"runtimeInjection": "<injection mode>"
// JSDefender runs in quiet mode. Default: off
"quietMode": true,
// Specify the license key. It is usually better to specify it via environment or CLI argument
"license": "<license key>",
// The settings for the same command line options
"esTarget": "<es5, es2015, etc.>",
"randomSeed": 12345,
"ignoreUnsafeConstructs": false,
"disableInline": false,
// The protection options to use globally
"settings": {},
// Named configuration sets (see explanations later)
"namedSets": {}
}
inputs
: This section defines the list of input source files and allows configuring their protection on a file-by-file basis. It must contain at least one entry unless you specify exactly one input and one output file in the command line arguments. In the latter case,inputs
can be omitted or may contain an empty list.sourceDir
: Optional input directory. When specified, input files are relative to this folder within the working directory. If omitted, input files are relative to the working directory. Note: JSDefender does not collect JavaScript files within this folder. You have to specify them one-by-one in theinputs
section.outDir
: Optional output directory relative to the working directory. JSDefender puts the protected files into this folder with their original name. Default:protected
.mapFile
: Optional map file name. Specifying this value changes the default map file name (lexical-map.json
). Use"off"
to turn off map file generation.runtimeFile
: Optional file name. Set this value only if you want to put the JSDefender Runtime into a separate file.runtimeInjection
: Specifies the way the JSDefender Runtime is injected into the protected code.quietMode
,license
,estarget
,randomSeed
,ignoreUnsafeConstructs
,disableInline
: All of these are optional. They reflect the similar command line options.settings
: Optional protection configuration to use for all input files, unless the particular configuration of a specific file overrides it. This value is an object with several properties, described in Protection settings.namedSets
: Optional hash object for named configuration sets, which helps you group frequently used configuration settings. You can learn more details in the Named configuration sets section.
Input files
The inputs
property in the configuration file represents the input files as an array. Each element in this array is either a string or an object specifying more details. When an item is a string, the value specifies the location within the sourceDir
. Instead of a simple string, you can use a descriptor object with these properties:
in
: Mandatory, specifies the name of the input file just as if you'd used a simple string.out
: Optional, specifies an alternative name or path for the input (instead of the default location and name withinoutDir
).protection
: Optional. Allows using a string key to a named set. When this property is set, the protection options within the named set are used instead of the ones in thesettings
property.isModule
: Optional boolean value. Set to true if the corresponding input file is loaded as a module (e.g, with a<script src="..." type="module"></script>
tag).
Examples:
// Simple scenario
{
"inputs": [ "code1.js", "code2.js" ]
}
// Do not protect the vendor1.js file
{
"inputs": [
"code1.js",
"code2.js",
{
"in": "vendor1.js",
"protection": "off"
},
"vendor2.js"
]
}
// Move vendor files into a subfolder of the output
{
"inputs": [
"code.js",
{
"in": "vendor1.js",
"out": "/vendors/vendor1.js"
},
{
"in": "vendor2.js",
"out": "/vendors/vendor2.js"
},
]
}
Runtime injection
With the runtimeInjection
configuration option, you can specify how the JSDefender Runtime (a collection of core protection utilities) gets injected into the protected source code. Its value can be a string or an object.
When you use a string, it can be one of these values:
firstNonModule
: Injects the Runtime into the first non-module source file (default, when you do not setruntimeFile
).separateSource
: Injects the Runtime into the file given with theruntimeFile
option (default, when you use an explicitruntimeFile
).all
: Injects the Runtime into each protected file that requires it.
Instead of a string, you can use an object with these properties:
mode
: an optional property with the values above to define the injection mode.options
: a required string the can be one of these values:self-defending
: protects the JSDefender Runtime with the self-defending transformation that observes code tampering.no-eval
: turns off the self-defending protection for JSDefender Runtime.
eval
function with a string literal once, and only once. Calling eval
is necessary for observing if an attacker tampers the code of JSDefender Runtime. The way the engine uses it prevents malicious code from being injected through eval
.
If your CSP compliance rules do not allow eval
in your JavaScript code, you can use the no-eval
option. Please, be aware that the JSDefender Runtime protected with the no-eval
option is not as strong as the default (self-defending) protection. We encourage you to avoid no-eval
whenever possible.
Examples:
// Inject the JSDefender Runtime into all input files
{
"runtimeInjection": "all"
}
// Instruct the protection engine to turn off self-defending
// JSDefender Runtime:
{
"runtimeInjection": {
"options": "no-eval"
}
}
Protection settings
The settings
property of the configuration file holds an object where each property names a protection transform, and its value specifies the parameters of that transform. This JSON code snippet shows the schema of settings
:
{
"boleanLiterals": "<value>",
"constantArgument": "<value>",
"controlFlow": "<value>",
"dateLock": "<value>",
"debuggerRemoval": "<value>",
"domainLock": "<value>",
"exprSequence": "<value>",
"functionReorder": "<value>",
"integerLiterals": "<value>",
"localDeclarations": "<value>",
"propertyIndirection": "<value>",
"propertySparsing": "<value>",
"selfDefending": "<value>",
"stringLiterals": "<value>",
"variableGrouping": "<value>"
}
Here, "<value>"
is just a placeholder for property values, which are different for each particular property. All properties are optional; you can leave them from the configuration. Omitting a property instructs the protection engine to inherit the value of the property from another configuration entity.
Setting <value>
to null
or false
turns off the associated protection. Specifying true
for <value>
turns on the particular protection with its default setting, except domainLock
, and dateLock
, which do not have a default.
Several properties, constantArgument
, debuggerRemoval
, exprSequence
, propertyIndirection
, propertySparsing
, stringLiterals
, variableGrouping
, and variableMasking
, respectively, can have only a Boolean values. So you can choose from turning them on or off, but you have no other options.
Here are the configuration details of the other properties:
booleanLiterals
The value can be either a Boolean value or an object with the randomize
property that turns on or off (default) randomization for this transform. Let's see a few examples:
// Turn this transform off
{
"booleanLiterals": false
}
// Turn this transform on, without randomization
{
"booleanLiterals": true
}
// Turn this transform on, without randomization
{
"booleanLiterals": {}
}
// Turn this transform on, with randomization
{
"booleanLiterals": {
"randomize": true
}
}
// Turn this transform on, without randomization
{
"booleanLiterals": {
"randomize": false
}
}
controlFlow
The property value can be a Boolean (turns on or off the associated transform), or an object with these optional properties:
randomize
: Boolean that turns on or off (default) randomizing values used for control flow obfuscation.injectFakeCode
: Boolean that allows or disables (default) fake code branches into the final state machine that implements the control flow.
Examples:
// Turn this transform off
{
"controlFlow": false
}
// Turn this transform on (#1)
{
"controlFlow": true
}
// Turn this transform on (#2)
{
"controlFlow": {}
}
// Turn randomization on
{
"controlFlow": {
"randomize": true
}
}
// Turn randomization and fake code injection on
{
"controlFlow": {
"randomize": true,
"injectFakeCode": true
}
}
dateLock
While you can turn off this protection option by setting its value to false
, you cannot turn it on with true
, as it does not have a default value to apply. Writing this will raise an error:
{
"dateLock": true
}
Datelock accepts a configuration object with three optional properties:
startDate
: The inclusive start date of the lock period. If not specified, any date not later thanendDate
is accepted.endDate
: The exclusive end date of the lock period. If not specified, any date not earlier thanstartDate
is accepted.errorScript
: Optional string. You can provide a valid JavaScript code snippet in this property. This script is executed when the protection senses that the code runs out of the specified date range. For more details, see the Error Scripts section.
Though both properties are optional, you need to specify at least one of them. Both expect a string using the ISO-8601 date format (YYYY-MM-DDTHH:MM:SSZ
, where T
is the separator between date and time). You can omit the time part from the property values.
Examples:
// Lock the code from (and after) April 30, 2020, 11:00AM UTC
{
"dateLock": {
"endDate": "2020-04-30T11:00"
}
}
// Allow this code running from (and after) May 15, 2020, 3PM UTC
{
"dateLock": {
"startDate": "2020-05-15T15:00"
}
}
// Allow this code to run only on April 1, 2020
{
"dateLock": {
"startDate": "2020-04-1",
"endDate": "2020-04-02"
}
}
// Allow this code to run only in year 2020. Display an alert
// dialog when running in other dates.
{
"dateLock": {
"startDate": "2020-01-01",
"endDate": "2021-01-01",
"errorScript": "alert('It is not 2020 anymore')"
}
}
domainLock
While you can turn off this protection option with setting its value to false
, you cannot turn it on with true
, as it does not have a default value to apply. Writing this will raise an error:
{
"domainLock": true
}
This property accepts a string or a configuration object with these properties:
domainPattern
: Mandatory string. You can set it to a full domain name likewww.mydomain.net
, or a partial domain name like.mydomain.net
to allow the code run in any subdomain ofmydomain.net
.errorScript
: Optional string. You can provide a valid JavaScript code snippet in this property. This script is executed when the protection senses that the code runs from an invalid domain. For more details, see the Error Scripts section.
.mydomain.net
, it matches with www.mydomain.net
. However, a hacker cannot defeat this protection by registering a domain like www.mydomain.net.example.com
, as this name does not match with the specified pattern. The mydomain.net
pattern allows code to be loaded from mydomain.net
, but not from any of its subdomains.
When you use a string value, it has the same semantics as the domainPattern
property. You can bind the code to multiple domains: specify multiple domain patterns separated by a semicolon.
Examples:
// Code that runs on preemptive.com (#1)
{
"domainLock": {
"domainPattern": "preemptive.com"
}
}
// Code that runs on preemptive.com (#2)
{
"domainLock": "preemptive.com"
}
// Code that runs on mycompany.com and any subdomains of myorg.org (#1)
{
"domainLock": {
"domainPattern": "mycompany.com;.myorg.org"
}
}
// Code that runs on mycompany.com and any subdomains of myorg.org (#2)
{
"domainLock": "domainPattern": "mycompany.com;.myorg.org"
}
// Code that runs on any Acme Company website, and gives an alert
// when running from elsewhere
{
{
"domainLock": {
"domainPattern": ".acmecompany.com",
"errorScript": "alert('Invalid domain')"
}
}
functionReorder
The value can be either a Boolean value or an object with the randomize
property that turns on or off (default) randomization for this transform. Let's see a few examples:
// Turn this transform off
{
"functionReorder": false
}
// Turn this transform on (#1)
{
"functionReorder": true
}
// Turn this transform on (#2)
{
"functionReorder": {}
}
// Turn randomization on
{
"functionReorder": {
"randomize": true
}
}
// Turn randomization off
{
"functionReorder": {
"randomize": false
}
}
integerLiterals
The value can be either a Boolean value that turns on or off the transform or an object with detailed specification:
randomize
: An optional Boolean that turns on or off (default) randomizing values used for control integer literal replacement.radix
: An optional string or array of strings, which specify the radix to use when replacing integer literals. Accepted values are:"binary"
,"decimal"
,"hexadecimal"
, and"octal"
, respectively. When you specify multiple radixes, the protection engine will pick one at random to transform each literal.lower
: Optional value; the smallest integer literal to obfuscate (0-65535, default: 0).upper
: Optional value; the highest integer literal to obfuscate (0-65535, default: 8).
Let's see a few examples:
// Replace integer literals with no randomization
{
"integerLiterals": true
}
// Replace integer literals with randomization
{
"integerLiterals": {
"randomize": true
}
}
// Replace integer literals with their octal forms
{
"integerLiterals": {
"radix": "octal"
}
}
// Replace integer literals with their octal or binary forms at random
{
"integerLiterals": {
"radix": ["octal", "binary"]
}
}
localDeclarations
Accepts either a Boolean value that turns on or off the transform or an object with detailed specification:
nameMangling
: An optional string that describes the transformation to use when renaming variables. Available values are:"sequential"
,"hexadecimal"
,"base52"
, and"base62"
, respectively. Default is"base52"
.excludeIds
: An optional list of identifier strings. The top-level declarations with the specified names are not renamed.
Examples:
// Turn on local declaration
{
"localDeclarations": true
}
// Use "base62" name mangling
{
"localDeclarations": {
"nameMangling": "base62"
}
}
// Keep the "myFunc" and "acmeVar" top-level identifiers
// while using "sequential" name mangling
{
"localDeclarations": {
"nameMangling": "sequential",
"excludeIds": [ "myFunc", "acmeVar" ]
}
}
When you have a single identifier to exclude, you can omit the array delimiters:
{
"localDeclarations": {
"nameMangling": "base62",
"excludeIds": "getName"
}
}
The excludeIds
option allows you to use the "*" string as a shortcut to exclude the identifiers of all file-level declarations from renaming:
{
"localDeclarations": {
"nameMangling": "base62",
"excludeIds": "*"
}
}
For more flexibility, you can use regular expressions instead of strings. When you start the identifier pattern with "r:", JSDefender interprets it as a regular expression to match with variable names. For example, the following configuration disallows renaming any declarations with an identifier that contains her
:
{
"localDeclarations": {
"nameMangling": "base62",
"excludeIds": "r:her"
}
}
This sample will exclude identifiers ending with Func
:
{
"localDeclarations": {
"nameMangling": "base62",
"excludeIds": "r:Func$"
}
}
In some scenarios, you need to preserve the name of particular top-level identifiers, as renaming them may break running code. Let's assume you want to keep the getName
function identifier for this purpose. You can use the excludeIds
property of the localDeclarations
setting to list the names of IDs to preserve:
jsdefender.config.json
:
{
"inputs": ["file1.js", "file2.js"],
"settings": {
"localDeclarations": {
"nameMangling": "sequential",
"excludeIds": ["getName"]
},
"stringLiterals": false
}
}
JSDefender now keeps getName
, as the protected output reflects:
file1.js
:
function _0x000000() {
var _0x000001 = getName(); // ID kept
return "Hello, " + _0x000001 + ", from multifile demo!";
}
file2.js
:
function getName() {
// ID kept
return "Developer";
}
var _0x000003 = document.getElementById("msg");
_0x000003.textContent = _0x000000();
lexical-map.json
file that contains mappings between the original and mangled identifiers. You can learn about this feature in the Lexical map section.
selfDefending
This protection option accepts a Boolean value or an integer between 0 and 4 — or a configuration object. While the false
and 0
values turn off the protection, true
(which is equivalent to 1) and the other integers, turn it on.
You can use a configuration object with these properties:
level
: an integer number between 0 and 4, just as described earlier.errorScript
: Optional string. You can provide a valid JavaScript code snippet in this property. This script is executed when the protection senses that the code is tampered. For more details, see the Error Scripts section.
1
uses a single wrapper, 2
, 3
, and 4
add extra wrappers around the code, just like Matryoshka dolls.
Examples:
// Turn on self-defending protection (#1)
{
"selfDefending": true
}
// Turn on self-defending protection (#2)
{
"selfDefending": 1
}
// Turn on self-defending protection (#3)
{
"selfDefending": {
"level": 1
}
}
// Turn on three-folded self-defending protection
{
"selfDefending": 3
}
// ... or
{
"selfDefending": {
"level": 3
}
}
// Two-fold protection with error message
{
"selfDefending": {
"level": 3,
"errorScript": "alert('Code tampered!!!')"
}
}
This special protection comes with a price: the length of the protected code can be many times longer than the original. It may add significant overhead to the function execution, as it checks the integrity of the nested code.
JSDefender applies this protection only for IIFEs (Immediately Invoked Function Expression) that are recognized as once-executing. To allow other functions (or function-like constructs) to use this protection option, add explicit inline protection directives to the code. For example, even if selfDefending
is set to true
in the configuration file, it won't protect this top-level function:
function add(a, b) {
return a + b;
}
If you intend to protect it, turn on protection explicitly:
function add(a, b) {
"@jsdefender { selfDefending: true }";
return a + b;
}
Named configuration sets
The settings
property of the configuration is not the only place that you can define a set of options for protection transformations. If you intend to exercise partial protection, you may declare these options in several locations within your source code through inline directives. Often, you use only a few well-designed sets of configuration options within your code. It is very tedious and error-prone to repeat the same set of choices throughout your code.
JSDefender allows you to utilize named configuration sets. The optional namedSets
property is a hash object. Each property name within this object is a unique configuration key. The property value is an object with the same structure as settings
, and declares a set of protection transforms associated with the key. Let's see an example:
{
"namedSets": {
// Minimal performance impact
"light": {
"booleanLiterals": true
},
// Minor performance impact
"medium": {
"booleanLiterals": {
"randomize": true
},
"integerLiterals": {
"radix": "hexadecimal"
},
"stringLiterals": true
},
// Stronger protection, but has performance impact
"heavy": {
"booleanLiterals": {
"randomize": true
},
"integerLiterals": {
"radix": "hexadecimal"
},
"stringLiterals": true,
"propertyIndicrection": true,
"localDeclarations": {
"nameMangling": "sequential"
},
"controlFlow": true
}
}
}
This configuration snippet specifies three named sets, light
, medium
, and heavy
, respectively. Instead of repeating the values behind those keys, you need to use only the property names. Let's see a few examples!
In the inputs
section, you can define the protection settings to apply for a particular file:
{
"inputs": [
"functions.js",
{
"in": "code.js",
"protection": "medium"
},
{
"in": "myIP.js",
"protection": "heavy"
}
]
}
This input declaration specifies that the code.js
file should use the medium
settings, while myIP.js
the heavy
options.
Assume that there's a section of code within myIP.js
that can be used with the medium
settings, as that does not contain real intellectual property. In the code, with inline protection directives, you can set that level:
function mySimpleFunctionWithNoRealIP() {
"@jsdefender medium"; // Here we set the "medium" options
// ...
}
If later you decide that medium
should use octal integer literals, you carry out this change in a single location, within the namedSets
configuration property:
{
"namedSets": {
// ...
"medium": {
"booleanLiterals": {
"randomize": true
},
"integerLiterals": {
"radix": "octal" // We changed this line
},
"stringLiterals": true
},
// ...
}
}
Special named sets
JSDefender defines a few special named sets that can be applied everywhere a named set key is expected: both in the configuration file and within inline protection directives:
off
: Turns off protection.disable-inline
: Ignores inline protection directives.default
: Uses the default configuration options of JSDefender.
Configuration merging
When JSDefender runs, it uses this sequence to decide what protection configuration setting should be applied for a particular part of the code:
- JSDefender reads the configuration file:
- If the command line options specify an explicit configuration file, JSDefender reads that file.
- Else, if there is a
jsdefender.config.json
file in the current working directory, JSDefender obtains that one. - Otherwise, JSDefender uses its default settings.
- JSDefender overrides configuration options read in the previous steps with the ones specified in the command line arguments. Thus, command line arguments take priority over the configuration file settings.
- Choosing the top-level configuration settings for each input file:
- If the input file specification uses a
protection
property, JSDefender uses the named set given there. - Else, it uses the protection in the
settings
part of the configuration, as calculated in the previous two steps.
- If the input file specification uses a
- Applying top-level inline protection configuration for a particular input file: Unless the
protection
isdisable-inline
, the top-level inline protection directive is merged with the top-level file protection settings calculated in the previous step. The inline protection directives take priority over the top-level file settings. - Applying nested inline protection configuration: An inline protection configuration directive in a nested scope (e.g., one within a function) is merged with its parent scope; the nested scope takes priority.