DEV Community

VEILVM
VEILVM

Posted on

JavaScript Anti-Debugging and Tamper Detection

Reading code is only half of what attackers do. The other half is running it under a debugger and changing it: skipping a license check, freezing a timer, or hooking a function to steal its output. Anti-debugging and tamper detection try to make that harder. This guide covers the common techniques, why most of them fail on their own, and how to use them so they actually raise the cost.

A realistic goal

No client-side check can stop a determined person who controls the browser. They can patch the engine, use a modified browser, or rewrite your file with DevTools local overrides. The realistic goal is to:

  • make the first attempts noisy and slow
  • force the attacker to find and remove every check
  • make removal break something important
  • give your server signals it can act on

With that framing, here are the building blocks.

Anti-debugging techniques

The debugger statement trap

The classic trick calls debugger repeatedly. With DevTools closed it does nothing; with DevTools open, execution pauses constantly.

setInterval(function () {
  const start = performance.now();
  debugger;
  if (performance.now() - start > 100) {
    onDebuggerDetected();
  }
}, 1000);
Enter fullscreen mode Exit fullscreen mode

The timing check detects that a pause happened. Weak points: DevTools has "Never pause here" and "Deactivate breakpoints", and the interval is easy to find and delete.

Timing checks around sensitive code

Single-stepping makes code run orders of magnitude slower. Measuring elapsed time across a critical section can reveal it. Keep thresholds generous: slow phones, background tabs and garbage collection pauses also cause delays.

DevTools detection heuristics

Older tricks relied on window size differences or console getters firing when DevTools formatted an object. Browsers have changed and many of these give false positives or stop working. Treat them as weak hints, never as proof.

Native function integrity

Attackers often hook fetch, XMLHttpRequest, Function.prototype.toString or crypto.subtle to log values. You can check that natives still look native:

function isNative(fn) {
  return typeof fn === "function" &&
    /\{\s*\[native code\]\s*\}/.test(Function.prototype.toString.call(fn));
}
if (!isNative(window.fetch)) reportTamper("fetch-hooked");
Enter fullscreen mode Exit fullscreen mode

A careful attacker also patches toString, so capture references to natives as early as possible, before other scripts run.

Tamper detection techniques

Self-hashing

Compute a hash of a function's source or of a protected data blob at runtime and compare it with a value embedded at build time.

function checksum(str) {
  let h = 0x811c9dc5;
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 0x01000193);
  }
  return h >>> 0;
}
const ok = checksum(verifyLicense.toString()) === BUILD_HASH;
Enter fullscreen mode Exit fullscreen mode

If someone edits verifyLicense to always return true, the hash changes. Of course, they can also patch the comparison, which is why where you put the check matters (see below).

Environment and context checks

Check that code runs where it should: the expected origin, the expected Electron context, not inside an iframe on another domain, not in a headless browser with obvious automation flags. These catch copied games and scripted abuse.

Server-verified integrity

The strongest tamper signal is one the server checks. The client computes a value from protected logic plus a server-issued nonce; the server knows what the correct value is. A patched client either cannot produce it or produces a wrong one. This also stops attackers from simply copying your function into their own bot, because the nonce is short-lived and tied to a session.

Why plain-JS checks fail

If your anti-debugging code is readable JavaScript, the attack is simple: search for debugger, performance.now or toString, delete the check, done. Even after standard obfuscation, AST tools and a debugger usually locate these checks quickly. How attackers deobfuscate JavaScript explains that workflow.

Three things make checks much harder to remove:

  1. Put them inside protected code. If the integrity check lives inside the same VM-protected function as your license logic, there is no separate if statement to delete. Removing it requires reversing the interpreter first, which greatly raises the cost of reverse engineering.
  2. Entangle results with real work. Instead of if (tampered) exit(), fold the check result into a value the program needs, such as a decryption offset or a key derivation input. A removed check then produces subtly wrong output instead of an obvious crash site.
  3. Delay the reaction. Immediate errors point straight to the check. Reporting to the server, degrading features later, or flagging the session for review gives the attacker less feedback.

Do not punish real users

Bad anti-debugging causes real problems:

  • Infinite debugger loops freeze tabs for developers investigating unrelated issues and for accessibility tools.
  • Aggressive timing checks misfire on slow devices.
  • Crashes on detection generate support tickets and bad reviews.

Prefer soft responses: log a signal, require a fresh server check, or disable a premium feature. Reserve hard blocks for signals you are sure about, like failed server-side verification.

Where these techniques fit

FAQ

Can JavaScript detect if DevTools is open?

Only with heuristics such as timing around debugger statements, and modern browsers make many tricks unreliable. Use detection as a weak signal and avoid blocking users based on it alone.

Is anti-debugging enough to protect my code?

No. On its own it is easy to find and remove. It becomes useful when it is placed inside protected code, combined with tamper checks, and backed by server-side verification.

Does anti-debugging code hurt performance?

Lightweight checks every few seconds cost almost nothing. Tight loops with debugger statements or heavy hashing on every frame can, so keep checks infrequent and outside hot paths.

Try protected integrity checks

If you want your integrity checks to live inside bytecode rather than readable JS, VEILVM can VM-protect a function in the browser, no sign-up needed: 3 free runs a day, files up to 5 KB (resets at 00:00 UTC). Standard requests are processed in memory and your code is never written to logs. Try it at https://vm.ldvmp.com.

Top comments (0)