devtools-terminator
v0.1.3
Published
A lightweight browser security library that detects and prevents Developer Tools access by terminating the session
Maintainers
Readme
DevTools Terminator
A lightweight browser security library that detects and prevents Developer Tools access by terminating the session.
DevTools Terminator detects when a user opens browser Developer Tools and immediately terminates their session by wiping all locally stored data and redirecting to a termination page. It uses two independent detection mechanisms (console getter trap and viewport differential), keyboard interception, and full storage sanitization.
The entire library is written in pure JavaScript with zero runtime dependencies for the client.
Table of Contents
- About
- Features Overview
- Which Version Should I Use?
- Git Clone Users
- npm Users
- Configuration Reference
- How It Works
- Public API Reference
- Browser Compatibility
- Security Considerations
- Contributing
- Changelog
- License
About
Part of the GimiRick toolchain. We build open source LLMs and AI systems. Founded by Mohammad Faiz.
Features Overview
Client-Only Mode
- Console getter trap — monitors console access via a property getter on a plain object, logged every 200ms (keeps a live entry in Chrome's ring buffer)
- Viewport differential — detects DevTools docked to the side (150px width diff) or bottom (170px height diff), checked every 200ms
- Keyboard interception — blocks F12, Ctrl+Shift+I/J/C, Ctrl+U and macOS equivalents
- UI protection — disables right-click, text selection, and drag-and-drop
- Full storage wipe — clears localStorage, sessionStorage, cookies, IndexedDB, CacheStorage
- Service Worker cleanup — unregisters all service workers on termination
- Mobile detection — automatically suppresses viewport checks on mobile devices
- No dependencies — zero npm packages or CDN resources required
- CSP compatible — no eval, no new Function(), no inline event handlers
Hybrid Mode
All Client-Only features, plus:
- Cryptographic heartbeats — HMAC-SHA256 signed proof-of-life signals every 30 seconds
- Browser fingerprinting — SHA-256 hashed user agent + screen + timezone
- Script integrity verification — server validates the hash of the running script
- Replay attack protection — timestamp-validated payloads with configurable window
- Server-enforced termination — terminated sessions cannot access protected routes
- Fingerprint & IP blocking — terminated sessions block the originating device fingerprint and IP, preventing bypass by requesting a new session ID
- Termination beacon — fire-and-forget notification via
fetch({ keepalive: true })(withnavigator.sendBeaconfallback) - Audit logging — server-side security event hooks for custom alerting
- Memory management — automatic cleanup of stale and terminated sessions
- Rate limiting — per-IP throttling on heartbeat, terminate, and session endpoints
- Request body limits — configurable max body size prevents OOM attacks
- Structured logging — JSON-formatted logs with levels and custom logger support
Which Version Should I Use?
| If you have... | Use... | | --- | --- | | A static website, demo, or frontend without a backend | Client-Only — no server needed, works immediately | | A Node.js/Express application | Hybrid — server-enforced security with heartbeat validation | | Low-security requirements (blog, portfolio) | Client-Only — sufficient for casual deterrence | | High-security requirements (admin panel, SaaS) | Hybrid — cryptographically validated session security | | No ability to modify server code | Client-Only — drop in and go |
Git Clone Users
This section is for users who obtain the library via git clone.
Prerequisites
- Node.js 20+ (required for the Hybrid server demo and npm operations)
- A local HTTP server for the Client-Only demo (any will do)
Clone
git clone https://github.com/GimiRick/DevTools-Terminator.git
cd DevTools-TerminatorInstall Dependencies
npm installThis installs the Express dependency required for Hybrid server mode. The client library itself has no dependencies.
Project Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ DEVTOOLS TERMINATOR │
│ Browser Security & Anti-DevTools Library │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────────────┐ ┌───────────────────┐ ┌──────────────────────────┐
│ CLIENT-ONLY MODE │ │ HYBRID MODE │ │ SERVER MIDDLEWARE │
│ (devtools-terminator)│ │(hybrid variant) │ │ (devtools-terminator- │
│ │ │ │ │ server.js) │
│ ┌───────────────┐ │ │ ┌────────────┐ │ │ │
│ │ Detection │ │ │ │ Detection │ │ │ ┌────────────────────┐ │
│ │ Mechanisms │ │ │ │ Mechanisms │ │ │ │ Routes │ │
│ │ │ │ │ │ (same) │ │ │ │ │ │
│ │ • Console │ │ │ └─────┬──────┘ │ │ │ POST /heartbeat │ │
│ │ Getter Trap │ │ │ │ │ │ │ POST /terminate │ │
│ │ • Viewport │ │ │ ▼ │ │ │ GET /session │ │
│ │ W+H Diff │ │ │ ┌─────────────┐ │ │ │ 403 Check (all) │ │
│ │ (150/170px) │ │ │ │ Heartbeat │ │ │ └────────────────────┘ │
│ │ • Keyboard │ │ │ │ System │ │ │ │
│ │ Interception│ │ │ │ │ │ │ ┌────────────────────┐ │
│ │ │ │ │ │ HMAC-SHA256 │ │ │ │ Session Store │ │
│ │ │ │ │ │ │ │ │ │ (in-memory) │ │
│ └───────┬───────┘ │ │ │ Fingerprint │ │ │ │ (in-memory) │ │
│ │ │ │ │ Script Hash │ │ │ │ │ │
│ ▼ │ │ │ 30s interval│ │ │ │ lastHeartbeat │ │
│ ┌───────────────┐ │ │ └──────┬──────┘ │ │ │ terminated: bool │ │
│ │ Termination │ │ │ │ │ │ │ fingerprint │ │
│ │ Sequence │ │ │ ▼ │ │ │ scriptHash │ │
│ │ │ │ │ ┌─────────────┐ │ │ └────────────────────┘ │
│ │ 1. Atomic flag│ │ │ │ Beacon │ │ │ │
│ │ 2. Clear │ │ │ │ (keepalive) │ │ │ ┌────────────────────┐ │
│ │ intervals │ │ │ │ fire-and- │ │ │ │ Security Hooks │ │
│ │ 3. Callback │ │ │ │ forget │ │ │ │ │ │
│ │ 4. Wipe all │ │ │ └─────────────┘ │ │ │ onTermination() │ │
│ │ storage │ │ └────────┬──────────┘ │ │ onHeartbeat() │ │
│ │ 5. redirect │ │ │ │ └────────────────────┘ │
│ │ (replace) │ │ │ │ │
│ └───────┬───────┘ │ │ │ ┌────────────────────┐ │
│ │ │ └────────────────┼▶│ Production Guard │ │
│ ▼ │ │ │ (rejects default │ │
│ ┌───────────────┐ │ │ │ secret in prod) │ │
│ │ Storage Wipe │ │ │ └────────────────────┘ │
│ │ │ │ │ │
│ │ • localStorage│ │ ┌───────────────────┐ │ ┌────────────────────┐ │
│ │ • session │ │ │ CLI INIT │ │ │ Cleanup Routine │ │
│ │ Storage │ │ │ (npx devtools- │ │ │ (60s interval, │ │
│ │ • Cookies │ │ │ terminator init) │ │ │ stale >45s) │ │
│ │ • IndexedDB │ │ │ │ │ └────────────────────┘ │
│ │ • CacheStorage│ │ │ --client (default)│ │ │
│ │ • Service │ │ │ --hybrid │ └──────────────────────────┘
│ │ Workers │ │ │ --dir <path> │
│ └───────────────┘ │ │ --yes │
│ │ └───────────────────┘
└───────────────────────┘
│
┌─────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────────┐ ┌──────────────────┐ ┌──────────────────────────┐
│ PUBLIC / STATIC │ │ EXAMPLES │ │ DOCS │
│ │ │ │ │ │
│ terminated.html │ │ demo.html │ │ GETTING_STARTED.md │
│ (termination page) │ │ server-example.js│ │ HYBRID_SETUP.md │
│ │ │ usage-demo.js │ │ WHICH_FILES.md │
│ │ │ │ │ SECURITY.md │
│ │ │ │ │ CHANGELOG.md │
└─────────────────────┘ └──────────────────┘ └──────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONFIGURATION LAYER │
│ window.__DEVTOOLS_TERMINATOR_CONFIG__ (frozen after init) │
│ │
│ terminationURL │ windowSizeCheck │ blockKeyboard │ │
│ blockInteractions │ disableOnMobile │ onTermination │ hybridMode │
│ serverEndpoint │ sharedSecret │ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ PUBLIC API │
│ window.DevToolsTerminator (frozen read-only) │
│ │
│ version │ isTerminated() │ terminate() │ config │ _status() │
└─────────────────────────────────────────────────────────────────────────────┘Run the Client-Only Demo
Start any HTTP server in the project root:
npx http-server . -p 8080Then open http://localhost:8080/examples/demo.html in your browser. The page is protected by the Client-Only library. Try opening DevTools to see the termination sequence.
Run the Hybrid Server Demo
- Copy
.env.exampleto.envand generate a strong secret:
cp .env.example .env
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" >> .env- Start the server:
node examples/server-example.js- Create an HTML page that loads the hybrid client and points to the server.
Client-Only: Copy Files Into Your Project
Copy these files:
cp src/client/devtools-terminator.js /path/to/your/project/js/
cp public/terminated.html /path/to/your/project/public/Then add the script tag to your HTML:
<script>
window.__DEVTOOLS_TERMINATOR_CONFIG__ = {
terminationURL: '/terminated.html'
};
</script>
<script src="/js/devtools-terminator.js"></script>Hybrid: Copy Files Into Your Project
cp src/client/devtools-terminator-hybrid.js /path/to/your/project/js/
cp src/server/devtools-terminator-server.js /path/to/your/project/server/
cp public/terminated.html /path/to/your/project/public/See docs/HYBRID_SETUP.md for detailed server and client configuration.
npm Users
This section is for users who install the library via npm.
Install
npm install devtools-terminatorCLI Init Command
The fastest way to get started is the init command:
npx devtools-terminator initThis interactive command will:
- Ask whether you want Client-Only or Hybrid mode
- Copy the appropriate client JS file into your current directory
- Copy the
terminated.htmlpage into your current directory - Print a success message with next steps
You can also use flags:
npx devtools-terminator init --client # Client-Only mode (default)
npx devtools-terminator init --hybrid # Hybrid mode
npx devtools-terminator init --dir ./public # Specify target directory
npx devtools-terminator init --yes # Skip confirmation promptsThe init command will never overwrite existing files without asking for confirmation first.
Load via Script Tag
After running init, link the copied files:
<script>
window.__DEVTOOLS_TERMINATOR_CONFIG__ = {
terminationURL: '/terminated.html'
};
</script>
<script src="/path/to/devtools-terminator.js"></script>Or reference the file directly from node_modules:
<script src="/node_modules/devtools-terminator/src/client/devtools-terminator.js"></script>Import via Bundler (Vite, Webpack)
// Client-Only
import 'devtools-terminator';
// Or with explicit config
window.__DEVTOOLS_TERMINATOR_CONFIG__ = {
terminationURL: '/terminated.html'
};
import 'devtools-terminator';// Hybrid
window.__DEVTOOLS_TERMINATOR_CONFIG__ = {
hybridMode: true,
serverEndpoint: '/api/devtools',
sharedSecret: 'your-secret-key'
};
import 'devtools-terminator/hybrid';Import Server Module
const devtoolsMiddleware = require('devtools-terminator/server');
app.use('/api/devtools', devtoolsMiddleware({
sharedSecret: process.env.DEVTOOLS_SECRET,
rateLimitHeartbeat: 60,
rateLimitTerminate: 10,
rateLimitSession: 30,
maxBodySize: 10240,
logLevel: 'info'
}));Environment Variables
Create a .env file:
DEVTOOLS_SECRET=your-64-char-hex-string
PORT=3000Generate a strong secret:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Published Package
The npm package includes only:
src/— all source filespublic/terminated.html— the termination pageREADME.md— this fileLICENSE— MIT License
The following are excluded from the published package:
examples/— demos and usage examplesdocs/— documentation files.env.example— environment template
Configuration Reference
Configure the library by defining window.__DEVTOOLS_TERMINATOR_CONFIG__ before the script loads. The configuration object is frozen after initialization and cannot be modified at runtime.
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| terminationURL | string | '/terminated.html' | URL to redirect to on detection |
| windowSizeCheck | boolean | true | Enable viewport differential detection |
| blockKeyboard | boolean | true | Intercept DevTools keyboard shortcuts |
| blockInteractions | boolean | false | Block right-click, text selection, and drag |
| disableOnMobile | boolean | true | Suppress checks on mobile devices |
| destructiveClear | boolean | false | Wipe all browser storage (localStorage, sessionStorage, cookies, IndexedDB, CacheStorage, Service Workers) on termination |
| onTermination | function | null | Callback executed on detection (receives reason code) |
| hybridMode | boolean | true | Enable heartbeat system (Hybrid only) |
| serverEndpoint | string | '' | Server URL for heartbeats and beacons (Hybrid only) |
| sharedSecret | string | '' | HMAC key matching server config (Hybrid only) |
<!-- Default config (keyboard blocked, interactions allowed) -->
<script>
window.__DEVTOOLS_TERMINATOR_CONFIG__ = {};
</script>
<!-- Block interactions (right-click, text selection, drag) in addition to keyboard -->
<script>
window.__DEVTOOLS_TERMINATOR_CONFIG__ = {
blockInteractions: true
};
</script>
<!-- Allow keyboard shortcuts (keep interactions blocked) -->
<script>
window.__DEVTOOLS_TERMINATOR_CONFIG__ = {
blockKeyboard: false,
blockInteractions: true
};
</script>
<!-- Allow everything -->
<script>
window.__DEVTOOLS_TERMINATOR_CONFIG__ = {
blockKeyboard: false,
blockInteractions: false
};
</script>Server Middleware Configuration
Configure the server middleware by passing options to devtoolsTerminator():
const devtoolsMiddleware = devtoolsTerminator({
sharedSecret: process.env.DEVTOOLS_SECRET,
rateLimitHeartbeat: 60, // 60 heartbeats/min per IP
rateLimitTerminate: 10, // 10 terminate beacons/min per IP
rateLimitSession: 30, // 30 session creations/min per IP
maxBodySize: 10240, // 10KB max request body
logLevel: 'info', // error | warn | info | debug
logger: null // custom logger function (receives structured entries)
});| Option | Type | Default | Description |
| --- | --- | --- | --- |
| sharedSecret | string | 'change-this-to-a-random-secret' | HMAC key — must match client config |
| rateLimitWindow | number | 60000 | Rate limit window in ms |
| rateLimitHeartbeat | number | 60 | Max heartbeats per window per IP |
| rateLimitTerminate | number | 10 | Max termination beacons per window per IP |
| rateLimitSession | number | 30 | Max session creations per window per IP |
| maxBodySize | number | 10240 | Maximum request body size in bytes |
| logLevel | string | 'info' | Log level: error, warn, info, debug |
| logger | function | null | Custom logger — receives {time, level, msg, module, ...} |
| staleThreshold | number | 45000 | Session stale timeout in ms (no heartbeat grace) |
| replayWindow | number | 10000 | Replay attack prevention window in ms |
| cleanupInterval | number | 60000 | Stale session cleanup interval in ms |
| onTermination | function | null | Callback on termination — receives {sessionId, reason, timestamp, ip} |
| onHeartbeat | function | null | Callback on heartbeat — receives {sessionId, fingerprint, timestamp} |
When no custom logger is provided, the middleware writes JSON-formatted log entries to stdout (info, debug) and stderr (warn, error).
If the default secret is detected at startup, the middleware automatically generates a random 32-byte HMAC secret and logs a warning. Always configure a unique sharedSecret for multi-instance or production deployments.
How It Works
Console Object Property Getter
A plain object with an enumerable getter property is logged to the console every 200ms. When DevTools is closed, Chrome's console no-op stub stores a reference without evaluating the object's properties — the getter never fires. When DevTools opens and processes the buffered log entry, Chrome evaluates the object for display, triggering the getter and initiating termination.
On Firefox, getters are evaluated eagerly even during no-op stub processing, making this approach cross-browser compatible.
Viewport Dimension Differential
Two static checks run every 200ms:
- Width:
outerWidth - innerWidth > 150— catches DevTools docked to the right side. A 150px threshold safely clears extension sidebars (typically 50–120px) while reliably catching all DevTools panels (≥200px). - Height:
outerHeight - innerHeight > 170— catches DevTools docked to the bottom. The threshold is safely above browser chrome (typically 70–136px on Windows/Mac).
A delta tracking check complements the static checks by monitoring sudden drops in innerWidth or innerHeight while both outerWidth and outerHeight stay nearly constant. This catches the exact moment of DevTools side-docking or bottom-docking mid-session, even if the final panel is narrower than the static thresholds.
Termination Sequence
When any detection method fires, the following happens in strict order:
- An internal atomic flag is set to prevent the sequence from running more than once
- All active polling intervals are cleared
- Any user-defined callback function is executed
- In Hybrid Mode: a termination beacon is sent to the server via
fetch({ keepalive: true })(preferred) ornavigator.sendBeacon()(fallback) - All local storage is wiped: localStorage, sessionStorage, cookies (across all domain variants), IndexedDB databases, CacheStorage entries
- All registered Service Workers are unregistered
- The browser is redirected to the termination page via
location.replace()(prevents back-button navigation)
Cryptographic Heartbeat System (Hybrid)
The browser sends a heartbeat to the server every 30 seconds. Each heartbeat contains:
- A browser fingerprint — SHA-256 hash of user agent + screen dimensions + timezone
- A script integrity hash — SHA-256 hash of the running script's source code
- A timestamp — used for replay attack prevention
The payload is signed with HMAC-SHA256 using a shared secret. The server verifies the signature using a timing-safe comparison, checks the timestamp is within the replay window, and updates the session's last-seen time.
If a heartbeat doesn't arrive within 45 seconds, or if the server receives a termination beacon, the session is marked as terminated. All subsequent API requests from that session get a 403 Forbidden response. The server keeps a list of terminated sessions and checks every incoming request against it.
However, the shared secret is embedded in the client code. Since it is a symmetric key (HMAC-SHA256) living in the browser, it is not a secret. A determined attacker can:
- Search the client JS for the secret string or set a breakpoint where the HMAC is generated and read the key from memory
- Write a short script using
requestsandhashlibto generate valid HMAC signatures - Route their traffic through a proxy (Burp Suite, mitmproxy, etc.) and feed valid heartbeats to the server
The server cannot cryptographically tell the difference between a real browser and a script forging heartbeats. Hybrid mode stops users who open DevTools, but it does not protect against a proxy-level attacker.
Session State Machine (Server)
Client Server
| |
|-- GET /session -------------->| Creates session entry (active)
|<-- { sessionId } ------------|
| |
|-- POST /heartbeat (30s) ---->| Validates HMAC, timestamp, updates lastHeartbeat
|<-- { status: 'ok' } ---------|
| |
|-- POST /terminate ---------->| Marks session.terminated = true
| | Adds to terminatedSessions
| |
|-- GET /api/* --------------->| Checks terminatedSessions → 403 if terminated
| |
|--- [45s no heartbeat] -------| cleanupStaleSessions() removes entryPublic API Reference
After initialization, the library exposes a read-only API on window.DevToolsTerminator:
| Member | Type | Description |
| --- | --- | --- |
| version | string | The current library version |
| isTerminated() | function | Returns true if the session has been terminated |
| terminate() | function | Manually triggers the full termination sequence |
| config | object | Read-only reference to the active configuration |
| _status() | function | Returns diagnostic info: viewport dimensions, mobile state, interval count |
Browser Compatibility
| Browser | Minimum Version | Platform | Status | | --- | --- | --- | --- | | Firefox | 88+ | Windows, macOS, Linux | Full support | | Safari | 14+ | macOS, iOS | Full support | | Microsoft Edge | 90+ | Windows, macOS | Partial (see note) | | Opera | 76+ | Windows, macOS | Partial (see note) | | Chrome | Desktop | Windows, macOS | Partial (see note) | | Chrome Mobile | Latest | Android | Full support | | Brave | Latest | All | Not supported (see note below) | | Vivaldi | Latest | All | Partial (see note) | | Arc | Latest | macOS | Partial (see note) |
Note on Chromium-based browsers (Chrome, Edge, Opera, Vivaldi, Arc): Chromium's no-op console stub (DevTools closed) does not evaluate getters on logged objects — the getter trap only fires when DevTools processes the buffered log entry. Repeated console.log(obj) every 100ms ensures a fresh entry is always in the ring buffer. Viewport detection provides the primary detection path on these browsers, catching both side-docked (150px width diff) and bottom-docked (170px height diff) DevTools. Undocked DevTools in a separate window remain a known fundamental limitation of JavaScript-based detection. This applies to all Chromium-based browsers listed above; they share the same DevTools console implementation.
Note on Brave (Shields feature): This library does not work on Brave Browser when its built-in Shields feature is enabled (Shields is enabled by default on Brave). Shields is Brave's privacy protection layer that blocks trackers and prevents browser fingerprinting. Multiple Shields protections directly interfere with the library's detection mechanisms:
window.outerWidth/window.outerHeightspoofing (viewport detection breakage): Brave Shields replaces these APIs with static, spoofed values (approximately 982×620) that do not reflect the actual browser window dimensions. The library's viewport differential detection relies on comparingouterWidth - innerWidth > 150andouterHeight - innerHeight > 170to detect docked DevTools. Since Brave Shields freezes these values regardless of DevTools state, the viewport check never triggers. Confirmed by Brave Community bug report.API farbling (fingerprinting randomization): Brave's fingerprinting protection (farbling) randomizes or blocks dozens of browser APIs per session and per site. The hybrid mode browser fingerprint (SHA-256 hash of
navigator.userAgent+screen.widthxscreen.height+Intl.DateTimeFormattimezone) produces inconsistent values because Brave spoofsuserAgent,screen.width, andscreen.height. This breaks the cryptographic heartbeat fingerprint binding.Console API modifications: Brave Shields can alter how the console buffers and evaluates logged objects, potentially breaking the console getter trap detection mechanism.
Since Shields is enabled by default on all pages in Brave, the library is effectively non-functional on this browser unless the user manually disables Shields for the site (click the lion icon in the address bar → toggle Shields off). This is a deliberate browser-level privacy protection that cannot be bypassed by JavaScript.
Other Chromium-based browsers (Chrome, Edge, Opera, Vivaldi, Arc) do not have Brave's Shields fingerprinting protection and function as described in the Chromium note above — viewport detection works, console detection works when DevTools is open, but the console getter trap is subject to the same Chromium no-op stub limitation.
Security Considerations
DevTools Terminator is a deterrent layer, not a replacement for server-side security. It raises the bar against casual and intermediate inspection but cannot stop a determined attacker with full control over their machine.
What it protects against:
- Casual users pressing F12 or right-clicking to inspect
- Users opening the browser console out of curiosity
- Automated scrapers that rely on DevTools APIs
- Brute-force and DoS attacks against server endpoints (via rate limiting)
- Memory exhaustion via oversized request bodies (via body size limits)
- Replay attacks (via HMAC timestamp validation with NaN-guarded window checking)
- Prototype pollution via crafted session IDs or IPs (stores use
Object.create(null)andMap)
What it does NOT protect against:
- Users who disable JavaScript (mitigated by the noscript fallback)
- Users who modify the script before it loads (mitigated by the hybrid script integrity check)
- Browser extensions that modify page content
- Browsers with built-in fingerprinting protection that spoofs window dimension APIs (e.g., Brave Shields)
- Proxy-based traffic interception
- Physical access attacks
For production applications, combine this tool with server-side authentication, Content Security Policy, Subresource Integrity, HTTPS, and backend authorization. See SECURITY.md for our security policy or docs/SECURITY.md for a full discussion.
The server middleware is hardened against common web attacks:
- Prototype pollution — all internal stores use
MaporObject.create(null), blocking__proto__key injection - Type coercion attacks — HMAC payload fields and session IDs are explicitly cast to
String()before validation; non-string session IDs are rejected with stricttypeofchecking - Replay bypass via NaN —
Number()+isNaN()guard on timestamps prevents payload replay with non-numeric timestamps - Express body-parser compatible — gracefully handles pre-parsed
req.bodyfromexpress.json()or other body parsers
Contributing
Contributions are welcome! All contributions must be in JavaScript — no TypeScript source files are accepted.
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
For detailed guidelines, see CONTRIBUTING.md.
Changelog
[0.1.3] — 2026-07-07
Fixed (0.1.3)
- Secure cookie deletion across origins: Cookie removal now writes
;Secureand;SameSite=None;Securecookie variants in addition to the bare cookie. When the page is served over HTTPS, cookies set withSameSite=None;Securefrom the original origin would not be cleared by a baredocument.cookie = ""overwrite — only a matching cookie with the identicalSecureflag can override it. Applied in both standalone and hybrid clients, andterminated.html. - Async storage wipe race:
clearAllStorage()now returnsPromise.all(p)andterminate()awaits the returned promise with a 1,000ms safety timeout. Previously, IndexedDB, CacheStorage, and service worker unregistration (all async) would continue after the response was sent, andterminate()returned before these operations completed. - Cookie cleanup async gap: Cookie clearing now uses batch deletion (all domain/label variants) before the async wipe Promise chain, ensuring cookies are cleared synchronously even if the subsequent async wipe takes time.
- Session ID leaked in URL query params:
sendHeartbeat()andsendTerminationBeacon()now only append?session=when usingnavigator.sendBeacon(which cannot send custom headers). Fetch calls use theX-Session-IDheader exclusively, preventing session IDs from appearing in server access logs, proxy logs, referrer headers, and browser history. - Fetch options mutation:
fetchWithTimeout()now shallow-clones theoptionsparameter before passing it tofetch(), preventing the caller's original options object from havingsignalinjected onto it. - Mobile false positive removal: Removed
screen.width < 768fromisMobile(). On desktop browsers with narrow viewports (e.g., windowed mode, ultrawide monitors with snapped windows),screen.widthreports the physical screen width — not the viewport — so this condition was never true on desktop and added zero value. Its presence created confusion during debugging. - Rate limiter clock drift vulnerability:
createRateLimiter()now checks for backward clock drift (timeSinceCleanup < 0) in addition to the forward stale-window check. When backward drift is detected, all rate-limit buckets are cleared unconditionally (not only those past theirresetAt), preventing stale buckets from falsely rate-limiting users for up towindowMsafter a clock jump. On forward drift, normal expired-bucket cleanup still applies. - Cleanup re-entry guard hardening:
cleanup()increateMemoryStore()now wraps its body intry/finally, ensuringthis._cleaningis always reset tofalseeven if an exception is thrown during cleanup iteration. - Instance export targeting: Static
module.exportsmethods (createSession,getSessionStore,getTerminatedSessions) now targetinstances[instances.length - 1](the latest initialized instance) instead ofinstances[0]. In multi-instance setups, the first initialized instance might already be stale or decommissioned.
[0.1.2] — 2026-07-07
Added (0.1.2)
fetchWithTimeout(url, opts, timeoutMs)helper withAbortController— hybrid fetch calls now time out after 5 seconds instead of hanging indefinitely- Session ID transmitted via
X-Session-IDheader (preferred) withsendBeaconquery param fallback — avoids leaking session IDs into server access logs, proxy logs, and browser history via URL - ESLint
no-restricted-syntaxrules to enforcevaroverlet/constin client files;caughtErrors: 'none'added tono-unused-varsrule - Hybrid mode startup warning when page is not served over HTTPS —
crypto.subtleandfetchrequire secure context destructiveClearconfig option (defaultfalse) — storage wiping (localStorage, sessionStorage, cookies, IndexedDB, caches, service workers) on termination is now opt-in to prevent accidental data losscreateMemoryStore()— structured store interface usingMapfor session and termination data with built-in fingerprint and IP blocking support- Fingerprint and IP blocking on termination — terminated sessions now block the originating fingerprint and IP, preventing bypass by requesting a new session ID from the same device or network
- Chunked cleanup (5,000 sessions per tick) — prevents event loop blocking when cleaning large session stores
- Integration test (
test/verification-simulation.js) — end-to-end server simulation validating the termination flow and IP blocking persistence - Viewport detection now checks both width (150px) and height (170px) — catches side-docked DevTools on Chrome/Chromium (width check safely above browser chrome, below typical extension sidebar widths)
- Viewport detection interval reduced from 1000ms to 100ms for near-instant response (later merged with duplicate console timer to 200ms — see Fixed section)
- Viewport delta tracking with outer dimension stability check (catches mid-session docking)
DevToolsTerminator._status()diagnostic method — returns current viewport dimensions,isMobile,windowSizeCheckstate and other debug info- Repeated
console.log(obj)keeps a live entry in Chrome's ring buffer (was once during init; the single entry could be evicted before DevTools opens; runs at 200ms after merging with viewport timer — see Fixed section)
Fixed (0.1.2)
- Prototype pollution hardening: rate-limiting
bucketsnow useObject.create(null)instead of plain objects ({}), preventing prototype pollution via crafted IP keys. Session and termination stores migrated toMapwhich is inherently immune to prototype pollution - Payload timestamp replay bypass:
Number(data.timestamp)withisNaN(payloadAge)guard prevents NaN-based replay window bypass. Previously, a non-numeric timestamp producedNaNinnow - data.timestamp, andNaN > cfg.replayWindowis alwaysfalse, allowing expired payloads to pass validation - Type coercion in HMAC payload construction: Explicit
String(data.fingerprint),String(data.scriptHash), andString(data.timestamp)prevent signature validation bypass if an attacker sends non-string values that produce different stringification in payload vs. the client's calculation.String(data.signature)ensurestimingSafeEqualreceives a string even if the attacker sends a non-string signature - Express body-parser compatibility: The server middleware now gracefully handles requests that have already been processed by
express.json()or other body parsers. By checkingreq.bodyinstead of solely relying onreq.on('data'), requests no longer hang indefinitely on consumed streams - Session ID type validation: Enforced strict string validation (
typeof id === 'string') inextractSessionId(). This prevents type coercion bugs (e.g.,"[object Object]") if an attacker maliciously passes arrays via duplicate query parameters like?session=1&session=2 - Performance: Dual redundant 100ms intervals merged into single 200ms interval — both client and hybrid scripts had two separate
setIntervaltimers each running at 100ms, both doing the same viewport + console detection work. Merged into one 200ms tick, reducing function call overhead by ~75% with no detection latency regression. The combined interval started at 250ms in initial implementation but was reduced to 200ms after testing confirmed Chrome's ring buffer reliability at shorter intervals - Critical hybrid sendBeacon regression: session ID was dropped from
sendBeaconfallback path during header migration —sendBeaconcannot send custom headers, so query param restoration was required for this code path - Shared
'anonymous'session ID collision: heartbeat and terminate handlers used'anonymous'as fallback session ID when none was provided — all anonymous sessions shared the same key, causing cross-session state corruption. Changed togenerateSessionId()for unique per-session identification - Hybrid client
fetchcalls no longer fail silently — all.catch()handlers now log errors viaconsole.warninstead of empty function bodies validateConfig()no longer clobbers falsy-but-valid configuration values (e.g.,logLevel: '') — now uses!= nullinstead of||for default application- Production guard now auto-generates a random 32-byte secret with a warning instead of crashing — more graceful, with a warning about multi-instance deployments
req.pathfallback (|| req.url) removed — Express 5.x always providesreq.path;req.urlin older Express included query strings which could break path matching- Fragile unit test pattern simplified — default-secret test no longer guarded behind
createSessionexistence check, removing conditional test execution - Critical Chrome/Mac false positive in
isMobile(): switched from'ontouchstart' in global(alwaystrueon Chrome/Mac due to Touch Bar event support) tonavigator.maxTouchPoints > 0(onlytrueon actual touch hardware). On Chrome/Mac,isMobile()was returningtrue, which disabled viewport detection entirely viadisableOnMobile— meaning NO detection mechanism was active on Chrome/Mac - Viewport thresholds adjusted: width 150px, height 170px — balanced to avoid extension sidebar false positives while reliably detecting DevTools
- Restored width-docked DevTools detection (was removed due to false positives from sidebar extensions at lower thresholds; 150px threshold avoids narrow extensions while catching all DevTools ≥200px)
- Height threshold set to 170px to safely clear Firefox power-user chrome (~165px max)
Changed (0.1.2)
blockInteractionsdefault changed fromtruetofalse— right-click blocking, text selection prevention, and drag protection are now opt-in. Sites behave more naturally unless explicitly configuredclearAllStorage()gated behinddestructiveClear: true— storage wiping on termination is now opt-in to prevent accidental data loss. A warning is logged when termination triggers butdestructiveClearisfalse- Session store migrated from plain objects to
createMemoryStore()usingMap— better dynamic key handling, built-in fingerprint and IP blocking, chunked cleanup (5,000 sessions per tick), and a clean store interface validateConfig()production guard changed from crashing to auto-generating a random secret with a warning — more graceful and preserves the running instance while alerting the operator- Server middleware refactored from module-level singleton stores to instance-scoped stores with
instances[]registry — eachcreateMiddleware()call now has fully isolated session and termination stores, preventing cross-instance state leaks when running multiple server instances - Server module exposed additional exports:
createSession,getSessionStore,getTerminatedSessions— backward compatible, all existing call patterns continue to work
Removed (0.1.2)
- Legacy
__DEVTOLS_TERMINATOR_CONFIG__and__DEVTOLS_TERMINATOR_INITIALIZED__fallback properties have been completely removed from client files to prevent accidental use of misspelled variables SEC_DEVTOOLS_FORMAT_005reason code (format probe function was already removed in an earlier iteration; constant was dead code)console.clear()removed from detection — was potentially interfering with Chrome's ring buffer processing- Debugger timing detection removed entirely — caused false positives on Chromium-based browsers
For full version history, see docs/CHANGELOG.md.
License
MIT License. Copyright (c) 2026 GimiRick.
See the LICENSE file for details.
