String encryption is not enough: attackers dump your JavaScript at runtime
String encryption hides literals in the file, not at runtime. This article explains how a runtime dump undoes string tables, why control-flow and VM protection raise the cost of finding when those strings are used, and why secrets still do not belong in client JavaScript.
A lot of JavaScript obfuscators lead with string encryption. Literals get packed into a rotated array, decoded by a small helper, and the shipped file no longer contains /api/internal/licenses or PLAN_PRO_MULTIPLIER in plaintext. People see that grep no longer works and assume API paths, license messages, and formula constants are safe.
They are not. The program has to decode a string the moment it uses it. That is exactly when an analyst dumps the value. String encryption changes how the file looks on disk. It does not change the fact that the running page holds the plaintext.
What string encryption actually does
The usual transform is mechanical. String literals are lifted into an array, optionally encoded (Base64, a XOR, a lightweight cipher), and every use site is replaced with a call to a decoder:
// before
fetch("/api/internal/quote");
const multiplier = 1.15;
// after (simplified)
var _s = ["L2FwaS9pbnRlcm5hbC9xdW90ZQ==", "MS4xNQ=="];
function _d(i) {
return atob(_s[i]);
}
fetch(_d(0));
const multiplier = Number(_d(1));
Real tools add rotation, extra indirection, and uglier names. The effect is the same. A casual search of the file for the API path fails. A competitor who only knows how to open the chunk and type Ctrl+F is slowed down. That is a modest benefit — and the entire benefit. The decoder and the encoded table are in the same file. The program must call _d to do any useful work. This is delayed readability, not secret-keeping.
Why a runtime dump undoes the table
Anyone who wants the strings does not need to reverse the encoding by hand. They wait until the decoder runs, then they record the output. Common approaches, all targeting the same moment — "the string has just become plaintext":
- Wrap the decoder (or
Function,eval, or a knownatobhelper) and log every argument and return value. - Set a breakpoint on the decoder and read the result in the Scope pane.
- Hook
fetch/XMLHttpRequest/WebSocketand read URLs after they have been decoded, or hookFunction.prototype.toStringif the tool reconstructs functions from strings. - Take a heap snapshot after the app has booted and search for distinctive plaintext you expect to exist (a product name, a path prefix, a license sentence).
A teaching sketch of the first approach:
// conceptual: wrap a decoder that the app already has to call
const original = _d;
_d = function (i) {
const value = original(i);
console.log("decoded", i, value);
return value;
};
You do not need a decompiler for this. You need the page to run. After a short session of clicking through the product, the console holds the API paths, the copy, the constants, and often the order in which they are used. If the constants matter — a pricing factor, a checksum salt you should not have shipped — they were only unindexed, not hidden.
Encryption of strings is not encryption of a secret
A secret that must remain a secret cannot be in the client, encoded or otherwise. An API token, a signing key, a webhook secret, or a private formula you would not put in a public gist does not become safe because _d(7) replaced the literal. The browser has to use the value, and a user of the browser can observe that use. Encoding changes the static file. It does not create an access-control system.
Treat any string the client needs as public data that is slightly inconvenient to search for. If that sentence is uncomfortable, the string should not be in the bundle.
What actually raises the cost
String tables fall over at first use because the use site is easy to find: a clearly named decoder, a cluster of _d(n) calls, a fetch that lights up in the Network panel. The next layer of protection is not a stronger cipher on the same table. It is making it harder to see when a value is decoded and how it is consumed.
Control-flow flattening scatters the decoder calls so a simple wrapper still produces a list of strings, but the surrounding logic is harder to read. That list is still useful — you get the paths — but turning the dump into a working copy of the algorithm takes longer.
VM-style protection goes further on the functions that consume the constants. Selected functions are compiled to a custom bytecode and executed by a small interpreter, so the original comparisons, branches, and formulas are not sitting in the file for a beautifier to restore. An analyst can still log plaintext at the edges — a fetch URL, a value written to the DOM. They have a harder time lifting the procedure that produced those values.
VEILVM is a browser-based JSVMP workspace you can point at one of those consumer functions to see the difference: the literals may still appear at runtime, but the function that used to be a page of readable arithmetic no longer is. Interpreted code is slower, so this is for a short list, not the whole app.
None of this is magic. If a decoded string is sent on the wire or shown in the UI, it will be visible there. Protection raises the cost of turning a dump into a reusable implementation. It does not unsay a credential.
A practical policy
- Do not put secrets in client strings. Move tokens, signing, and authoritative prices or entitlements to the server.
- Treat string encryption as a delay against casual search, not as a security control.
- If a constant must live in the client (an offline factor, a preview parameter, a domain-lock needle), protect the function that consumes it, not only the literal.
- Keep the official decision on the server, even if the client already computed a preview.
- Do not ship production source maps. A public map with
sourcesContentrestores the original literals and the code around them.
A short audit before a release:
# anything that looks like a secret still in source
grep -R -nE "SECRET|TOKEN|PRIVATE_KEY|SIGNING" src --include='*.js' --include='*.ts'
# confirm production maps are not in the deploy artifact
find dist -name "*.map"
grep -R "sourceMappingURL" dist || true
If a "hidden" admin path is your only authorization check, fix that first. Encoding the path does not authorize the caller.
Checklist
- No API keys, signing secrets, or private salts remain in the client, encoded or plaintext.
- String encryption, if you use it, is treated as a delay, not as secret storage.
- The functions that consume leftover sensitive constants are protected more heavily.
- Authoritative logic runs on the server.
- Production source maps are private or absent.
If you want to try VM protection on a function that consumes those constants, VEILVM runs in the browser with no sign-up: 3 free runs a day, up to 5 KB per file (resets at 00:00 UTC). A free account raises the input limit to 30 KB. Pro is $9/month or $79/year and adds an API so the protection step can run in CI. Standard requests are processed in memory and are not written to logs. Start at https://vm.ldvmp.com.
Comments
Join the discussionNo comments yet. Be the first to comment.

