A browser extension built for penetration testers and security researchers. Inspect, snapshot, diff, and export all JavaScript variables on any web page — including variables assigned live via the F12 browser console.
WARNING! This code is 100% vibe coded using Claude, so use at your own risk. I've only tested this on Firefox for linux and Android and it seems to work pretty well. I'm not liable for any damages caused to data, Firefox exploding, physical melting of an android device from using this extension blah blah blah.
- What is VarScope?
- Features
- How it works
- Installation
- How to use
- Restrictions and known limitations
- Known issues and future updates
- Project structure
- Technical deep-dive
- Customising the About panel
- License
- Contributing
When assessing a web application, the JavaScript global scope (window) is one of the most revealing attack surfaces available. Frameworks, authentication tokens, API endpoints, feature flags, user objects, and internal state are routinely exposed as global variables — often unintentionally.
VarScope gives you a clean, colour-coded view of everything sitting on window at any moment, separated into native browser built-ins and variables created by the target application. You can take a snapshot, interact with the page, then compare to see exactly what changed — helping you map how the application's state evolves during login, form submission, navigation between SPA routes, or after triggering specific functionality.
| Feature | Detail |
|---|---|
| 🟢 Native variable detection | Built-in browser and window properties shown in green |
| 🟡 Webapp variable detection | Variables created by the target site shown in yellow |
| 🔴 Diff / compare | Snapshot before, interact, compare after — changes in red |
| 🔍 MAIN world injection | Sees variables set via the F12 console, not just page-load vars |
| 💾 Session persistence | Scan results and snapshots survive closing and reopening the popup |
| 🔒 Isolated storage | All data stored in chrome.storage.local — completely invisible to the page |
| 📂 Expandable tree | Arrays and objects expand inline; nested values expand recursively |
| 🔎 Function source viewer | Click any function name to see its live source code |
| ✎ Variable editor | Edit any primitive, function, or array/object child live on the page |
| ✦ Prettify | One-click formatter for minified source — available in viewer and editor |
| 📤 Export | JSON (all), JSON (webapp-only), CSV, diff-only JSON, clipboard copy |
| 🔎 Filter and search | Search by name, value, or both — including rolled/nested child values |
| ↓ Load more | Truncated arrays and objects offer a link to fetch all remaining entries live |
| ⚙️ Configurable | Toggle auto-collapse, show/hide natives, functions, and undefined values |
| 📱 Firefox for Android | Full-screen responsive UI with touch-optimised controls |
Browser extensions normally run in an isolated world — a sandboxed JavaScript environment that is completely separate from the page's own JavaScript. Code running in the isolated world cannot read or modify window variables set by the page or by the browser console.
VarScope bypasses this by using chrome.scripting.executeScript with world: 'MAIN'. This injects the capture script directly into the page's own JavaScript execution context — the same scope used by the page itself and by everything you type into the F12 console. This means that if you type gerbil = "grebil" in the console and then click Scan, VarScope will find gerbil.
The capture script (content.js) iterates over the global scope using three complementary methods:
for...in window— enumerates all enumerable own and inherited properties, which includes everything the page has set as a global variable.Object.getOwnPropertyNames(window)— captures non-enumerable own properties thatfor...inwould miss.- Walking the prototype chain — catches properties defined on
Window.prototypeand its parents.
Each variable is then serialised — converted into a plain JSON-safe structure containing its type, a short preview string, and (for arrays and objects) its child entries up to a configurable depth and item count. Functions are captured as type function with a name preview; their source code is fetched separately on demand to avoid slowing down the initial scan.
VarScope maintains a blocklist of over 300 known native browser keys — every window property that is part of the browser platform itself: event handlers (onclick, onload…), timing functions (setTimeout, requestAnimationFrame…), DOM constructors (HTMLElement, Document…), Web APIs (fetch, WebSocket, crypto…), and JavaScript built-ins (Array, Promise, JSON…). Anything not on this list is classified as a webapp variable.
All scan data is written to chrome.storage.local, a private extension storage area. This storage is completely inaccessible to the page — the target site cannot read, write, or detect it via any DOM method, window property, or JavaScript API. Data is chunked into 400 KB pieces to avoid storage quota limits, reassembled transparently on load, and keyed by hostname so each site has fully independent state.
Because chrome.storage.local persists across popup open/close cycles, your scan results, snapshot, compare results, and filter settings are all automatically restored when you reopen the popup. A brief "↺ Session restored" toast confirms this. Data persists until you click Clear or the extension is removed.
VarScope requires Firefox 102+ or Chrome 95+. This minimum is driven by chrome.scripting.executeScript with world: 'MAIN', which was introduced in Firefox 102 (June 2022) and Chrome 95 (October 2021).
- Download the latest release zip from the Releases page
- Unzip it to a permanent folder (the browser loads from this folder every time — do not delete it after loading)
- Open your browser and navigate to
chrome://extensions(oredge://extensionsfor Edge) - Enable Developer mode using the toggle in the top-right corner
- Click Load unpacked
- Select the
varscope-extensionfolder (the one containingmanifest.json) - VarScope appears in your extensions list — click the puzzle icon in the toolbar and pin it for easy access
To update to a newer version: replace the folder contents with the new files, then go to chrome://extensions and click the refresh icon on the VarScope card.
The latest version can be found on the Mozilla Add-on page. Simply navigate to https://addons.mozilla.org/en-GB/firefox/addon/varscope/
Temporary installation (removed on browser restart):
- Download and unzip the latest release
- Navigate to
about:debugging#/runtime/this-firefox - Click Load Temporary Add-on…
- Browse into the
varscope-extensionfolder and selectmanifest.json - VarScope loads immediately and appears in your toolbar
Permanent installation:
Firefox requires extensions to be signed by Mozilla for permanent installation in the standard release build. Options:
- Sign the extension yourself via addons.mozilla.org/developers (free, requires a Mozilla account). The extension ID is
varscope@gerbilvarscope - Use Firefox Developer Edition or Firefox Nightly, which allow unsigned extensions to be permanently installed via
about:config→ setxpinstall.signatures.requiredtofalse - Use Firefox ESR with the same
about:configchange
VarScope v1.0.1+ supports Firefox for Android. The popup renders as a full-screen page on mobile, with larger touch targets, responsive layout, and mobile-optimised input handling.
Installation:
The latest version can be found on the Mozilla Add-on page. Simply navigate to https://addons.mozilla.org/en-GB/firefox/addon/varscope/
Firefox for Android requires extensions to be listed on addons.mozilla.org for easy install, or you can sideload using a custom collection:
- On your Android device, open Firefox → tap the three-dot menu → Settings → About Firefox
- Tap the Firefox logo five times rapidly to enable debug mode
- Go back to Settings → Install extension from file
- Select the
varscope-extensionzip or the extractedmanifest.json
Alternatively, use Firefox Nightly for Android which allows installing any signed or unsigned extension via about:debugging over USB from a desktop Firefox.
Usage on mobile:
- Tap the VarScope icon in the Firefox toolbar (or the extensions menu)
- The interface opens full-screen — tap Scan to inspect the current page
- All features work the same as desktop: snapshot, compare, export, function source viewer, variable editor
- The resize handle is not available on mobile (the viewport fills automatically)
- Dropdown menus (Export, Settings) are positioned to stay fully within the screen on all screen sizes
Known mobile limitations:
- The Send to new tab feature is currently disabled (see Known issues)
- Very large scan datasets (thousands of variables) may be slower to render on lower-end devices
- The function source modal fetches live source via script injection — this requires the page to be an
http://orhttps://URL
Navigate to any regular website (e.g. https://example.com), click the VarScope icon in the toolbar, and click Scan. You should see a list of variables appear within a second or two.
- Navigate to the target page in your browser
- Click the VarScope icon in the toolbar to open the popup
- Click ⟳ Scan
VarScope injects into the page's JavaScript context, captures all global variables, serialises them, and displays them in the popup. A count of variables found is shown in a toast notification and in the stats bar.
Note: You must be on a regular
http://orhttps://page. VarScope cannot inject into browser-internal pages — see Restrictions.
Every variable name is colour-coded to immediately distinguish what belongs to the browser from what belongs to the target application:
| Colour | Meaning |
|---|---|
| 🟢 Green | Native browser built-in — part of the browser platform, not the app |
| 🟡 Yellow | Webapp variable — created by the target site's JavaScript |
| 🔴 Red | Changed since the last snapshot (diff mode) |
Type badges next to each variable name are also colour-coded by data type:
| Badge colour | Type |
|---|---|
| Purple | function |
| Orange | string |
| Green | number |
| Amber | boolean |
| Cyan | array |
| Violet | object |
| Grey | null / undefined |
Diff status badges appear at the end of a row:
| Badge | Meaning |
|---|---|
CHANGED |
This variable existed in both snapshots but its value changed |
NEW |
This variable did not exist when the snapshot was taken |
REMOVED |
This variable existed in the snapshot but is no longer present |
Variables whose type is array or object show a ▸ toggle to the left of their name. Click it to expand the variable inline and see its contents. Each child entry shows its key (or array index), type badge, and preview value.
Nested values that are themselves arrays or objects also have a ▸ toggle — you can drill down as many levels as the data allows.
If a variable contains more items than the initial capture limit, a ↓ Load N more… link appears at the bottom of the expanded list. Clicking it fetches the complete contents of that specific variable live from the page.
Functions are shown by default (this can be toggled in Settings). When you see a variable with a purple function type badge, there are two ways to view its source:
- Click the variable name — the name is underlined and clickable
- Click the
view sourcebutton — a small grey button appears in the preview area
Either action opens the function source modal, which:
- Fetches the live source by injecting
fn.toString()into the page - Displays the raw source code in a scrollable
preblock - Provides a 📋 Copy button to copy the source to your clipboard
- Provides a ✦ Prettify button for non-native, non-trivial functions (see below)
- Provides a ✎ Edit button to open the source directly in the editor
Native functions: Many functions you encounter will show a purple banner: "This is a native browser function implemented in C++. The JS source is not available." Built-in browser functions such as setTimeout, Array.prototype.push, and document.querySelector are implemented in the browser engine itself. There is no JavaScript source to show, and they cannot be edited.
Prettify: For non-native functions, a ✦ Prettify button appears in the modal footer. Clicking it reformats the source with proper indentation, newlines, and spacing. Click ✦ Raw to restore the original verbatim output. A green ● prettified indicator shows which view is active.
The prettifier is a lightweight character-by-character formatter built into VarScope. It handles the majority of real-world minification patterns correctly, but is not a full AST-based formatter. For heavily obfuscated code, consider copying the source and pasting it into a dedicated tool such as Prettier.
Hover over any variable row to reveal a small ✎ button at the right side of the row. Clicking it opens the edit modal.
What can be edited:
- Primitive variables (string, number, boolean, null) — the textarea is pre-filled with the current value. Enter any valid JavaScript expression and click ✓ Apply (or press Ctrl+Enter). VarScope injects
window[key] = eval(yourExpression)into the page and immediately rescans to show the updated value. - Functions — the textarea is pre-filled with the live source. Edit the function body, parameters, or name, then click Apply. VarScope evals the new definition on the page.
- Array and object children — expand the parent variable with ▸ first, then hover over individual child rows. The ✎ button on a child row edits that specific element via
window[parentKey][childIndex] = eval(expr).
Edit button rules:
- The ✎ button does not appear on collapsed (rolled-up) arrays or objects at the top level. Expand them first so you can edit individual children rather than replacing the whole structure.
- The ✎ button does not appear on
REMOVEDdiff entries. - The ✎ button appears from the function source viewer as well (for non-native functions), making view → edit a single click.
Prettify in the editor: When editing a function, a ✦ Prettify button appears in the edit modal footer. Click it to format the source for easier reading and editing. Click ✦ Raw to restore the unformatted version. The prettified text is what gets applied if you click Apply while in prettified mode — VarScope evals whatever is in the textarea.
After a successful edit, a green confirmation shows for ~0.7 seconds, the modal closes, and the variable list rescans silently without resetting your scroll position.
The snapshot and compare workflow is one of VarScope's most useful capabilities for understanding how an application's state changes in response to user actions.
Workflow:
Scan → Snapshot → [interact with the page] → Compare
- Scan the page to capture the current state of all global variables
- Click ◎ Snapshot — this saves the current scan data as a baseline. A green snapshot bar appears below the toolbar showing the time the snapshot was taken
- Interact with the page — log in, submit a form, navigate to a different route, trigger a specific feature, or manipulate the application in any way you want to observe
- Click ⊞ Compare — VarScope performs a fresh scan and compares every variable against the snapshot
After comparing:
- Variables whose values have changed are highlighted red and show
old value → new value - Brand-new variables that didn't exist in the snapshot are marked
NEW - Variables that existed in the snapshot but are now gone are marked
REMOVEDand greyed out - The stats bar shows a count of changed variables
- A Changed filter chip appears, letting you show only the diff entries
- The diff state is persisted — close and reopen the popup and the comparison is still there
To clear a snapshot without clearing the full scan, click the ✕ clear link in the snapshot bar.
Snapshots and persistence: Both the snapshot and the compare result are stored in chrome.storage.local and survive popup close/reopen. They are only lost when you click Clear, take a new scan (which resets the diff but keeps the snapshot), or take a new snapshot.
The filter bar appears after the first scan:
Text search: Type into the search box to filter variables in real time. The filter is case-insensitive.
Search mode selector (in:): A small dropdown next to the search box controls what is searched:
| Mode | Behaviour |
|---|---|
| Name (default) | Matches against the variable name only |
| Value | Matches against the serialised value preview, including rolled/nested children |
| Both | Matches if either name or value contains the query |
When Value or Both is active, matching text in the preview column is highlighted in amber.
Filter chips:
| Chip | Effect |
|---|---|
| 🟢 Native | Show only native browser variables |
| 🟡 Webapp | Show only webapp-created variables |
| 🔴 Changed | Show only variables that differ from the snapshot (only appears after a compare) |
Filter chips are mutually exclusive for Native/Webapp. The Changed chip can be combined with either.
All filter and search state is persisted — if you close and reopen the popup, your filters are restored exactly as you left them.
Click the ↓ Export button to open the export menu:
| Option | Contents | Format |
|---|---|---|
| Export JSON (all) | Every variable including natives — type, preview, native flag | JSON |
| Export JSON (webapp only) | Webapp variables only — excludes all green (native) entries | JSON |
| Export CSV | All variables as a flat spreadsheet — key, native, type, preview | CSV |
| Export diff | Only CHANGED, NEW, and REMOVED entries — only available after a compare | JSON |
| Copy to clipboard | Plain-text list of all variables for pasting into notes or reports | Text |
Exported JSON files contain the variable name, a native boolean, the type string, and the serialised preview. They do not contain the full deep value of every nested object — for that, use the expand controls in the popup before exporting.
Click the ⚙ button to open the settings panel:
| Setting | Default | Description |
|---|---|---|
| Collapse arrays/objects by default | On | Arrays and objects start collapsed. Click ▸ to expand manually |
| Show native browser variables | On | Toggle the green native entries. Turn off to focus entirely on webapp variables |
| Show functions | On | Toggle function entries. Turn off to reduce noise from framework internals |
| Show undefined values | Off | Show variables whose value is null or undefined |
Settings are stored in chrome.storage.local and persist indefinitely.
Click the ? button to open the About panel, which shows the author name, version, GitHub repository link, and license. These values are hardcoded in the first three lines of popup.js and can be changed by editing that file:
var ABOUT_AUTHOR = 'gerbil';
var ABOUT_GITHUB = 'https://github.com/gerbilbyte/VarScope';
var ABOUT_VERSION = 'v1.1.0';VarScope cannot inject into browser-internal pages. If you click Scan on any of the following, a clear error message is shown and no injection is attempted:
| Page type | Examples |
|---|---|
| Browser settings | chrome://settings, chrome://extensions, edge://settings |
| Extension pages | chrome-extension://…, moz-extension://… |
| New tab page | chrome://newtab, about:newtab |
| Firefox internal | about:blank, about:debugging, about:preferences |
| Data URIs | data:text/html,… |
| JavaScript URIs | javascript:… |
Solution: Navigate to any regular http:// or https:// page before scanning.
Built-in browser functions — setTimeout, fetch, document.querySelector, Array.prototype.push, and hundreds of others — are implemented in C++ inside the browser engine. Their .toString() returns something like function push() { [native code] } because there is no JavaScript source to display. This is correct behaviour and not a bug. Native functions cannot be edited.
To reduce noise from native functions, you can:
- Use the 🟡 Webapp filter chip to show only app-created variables
- Turn off Show native browser variables in Settings
VarScope maintains a list of over 300 known native keys. However, browsers are updated frequently and add new APIs. If a native browser property is not on the blocklist, it will appear as a yellow webapp variable. This is a false positive rather than a false negative — you may occasionally see a browser built-in incorrectly classified as a webapp variable.
To keep the popup fast and avoid hanging on enormous data structures, VarScope serialises values to a maximum depth of 3 levels and caps arrays at 200 items and objects at 200 keys. Deeper values show as a collapsed Array(N) or {…} preview. Truncated lists show a ↓ Load N more… link that fetches the complete data live from the page.
The ✎ edit button is intentionally not shown on top-level array or object variables when they are collapsed. To edit individual elements, expand the variable with ▸ first — each leaf child then shows its own ✎ button. This prevents accidentally overwriting an entire array when you only want to change one element.
The function source viewer and editor work for top-level global functions (window.myFunc) and for one level of nesting (window.myObj.myMethod). Deeply nested functions beyond the first level of nesting may not resolve correctly if the path contains non-standard property names or computed keys.
The ✦ Prettify formatter handles most real-world minification well but is not a full AST parser. Heavily obfuscated code — especially code using character substitution, eval, or complex closure patterns — may not format cleanly. Copy the source and use a dedicated tool such as Prettier or de4js for obfuscated functions.
Firefox's chrome.storage.local implementation may behave differently to Chrome's in edge cases, particularly around quota limits for large scan datasets. If a scan on a very JavaScript-heavy site fails to persist, try using the 🟡 Webapp filter before scanning to reduce the data volume, or use the export options to save the data immediately after scanning.
The Send to new tab ↗ button is present in the popup but is not functional in the current release. The intention is to open the full scan in a dedicated browser tab with a wider table layout, making it easier to read large numbers of variables and review diffs.
The implementation requires reliable cross-context data transfer between the extension popup and a new extension page (tabview.html). This works consistently in Chrome but has reliability issues in Firefox due to differences in how the two browsers handle chrome.storage reads from newly opened extension pages. Rather than ship a feature that works on one browser and silently fails on the other, it has been disabled pending a proper fix.
This is planned as a future update.
varscope-extension/
├── manifest.json # MV3 manifest — permissions, icons, popup declaration
├── content.js # Variable capture script — injected into MAIN world
├── popup.html # Extension popup UI — layout, styles, all modal HTML
├── popup.js # All popup logic — scan, snapshot, diff, edit, render, storage
├── tabview.html # Full-tab view (planned — currently non-functional)
├── icons/
│ ├── icon16.png # Toolbar icon (16×16)
│ ├── icon48.png # Extension management icon (48×48)
│ └── icon128.png # Chrome Web Store / AMO icon (128×128)
├── LICENSE # PolyForm Noncommercial License 1.0.0
└── README.md # This file
content.js is a self-contained IIFE with no chrome.* API calls. It runs in world: 'MAIN' and must remain entirely self-contained — it cannot use any extension APIs. It builds the native key blocklist, serialises every global variable it finds, and returns the result synchronously.
popup.js handles everything else: reading the result from executeScript, writing to chrome.storage.local, building and diffing the variable tree, rendering the DOM, managing expand/collapse and edit state for nested values, fetching and editing function source code, prettifying source, and wiring all UI events.
popup.html contains all CSS as a single <style> block, the full HTML structure of the popup, and all modals (About, Function Source, Edit Variable).
Content scripts run in an isolated world by design — this is a browser security feature that prevents extension code from being manipulated by the page. However, it also means content scripts cannot see user-defined globals or variables set in the console. VarScope uses executeScript with world: 'MAIN' to bypass this intentionally, because reading page variables is the entire point.
The tradeoff is that code injected into world: 'MAIN' shares the page's execution context and could theoretically be read or interfered with by the page. VarScope mitigates this by keeping content.js stateless — it runs, captures, and returns immediately without leaving any persistent state on window.
The binding constraint is chrome.scripting.executeScript with world: 'MAIN', which requires:
- Firefox 102.0 (released June 2022) — this is the
strict_min_versionin the manifest for bothgeckoandgecko_android - Chrome 95 (released October 2021)
Other APIs used (chrome.storage.local, chrome.tabs.query, chrome.runtime.lastError) have been available since much earlier versions and are not the limiting factor.
The Firefox extension ID is varscope@gerbilvarscope. This is set in the manifest under browser_specific_settings.gecko.id and is used by Firefox to identify the extension for storage, updates, and AMO signing.
VarScope collects no user data. The data_collection_permissions field in the manifest declares:
"data_collection_permissions": {
"required": ["none"],
"optional": []
}All scan data is stored locally in chrome.storage.local and never transmitted anywhere. The extension makes no network requests of its own — it only injects scripts into the active tab's page context when the user explicitly clicks Scan, Compare, or a function/edit button.
State is serialised as a single JSON blob containing:
{
"v": 3,
"vars": { "myVar": { "native": false, "serialized": { "type": "string", "preview": "\"hello\"" } } },
"snapshot": { ... },
"snaptime": "2025-04-18T12:00:00.000Z",
"diff": { "myVar": { "status": "changed", "oldPreview": "\"hello\"", "newPreview": "\"world\"" } },
"ui": { "filterNative": false, "filterWebapp": true, "filterChanged": false, "searchQuery": "", "filterBy": "name" }
}This blob is chunked into 400 KB pieces and stored under keys vs_h_{storeKey} (header with chunk count) and vs_c_{storeKey}_0, vs_c_{storeKey}_1, etc., where storeKey is derived from the page hostname. All chunks are written atomically. Previous chunks are deleted before writing new ones to prevent stale data.
The compare operation performs a fresh scan and then computes a flat diff:
- Keys present in the new scan but absent from the snapshot →
NEW - Keys present in the snapshot but absent from the new scan →
REMOVED - Keys present in both where the serialised preview differs →
CHANGED
For CHANGED entries, a second-level child diff is computed: array items are compared by index, object properties by key. Any child whose preview has changed is flagged individually so that when you expand a changed variable, the specific item that changed is highlighted in red.
The prettifier in popup.js is a hand-written character scanner that tracks:
- String literal boundaries (
",',`) including escape sequences — so it never breaks string contents - Line and block comment regions — so
//and/* */comments are preserved intact - Brace depth —
{increments and}decrements the indent level - Semicolons — trigger a newline at the current indent level
- Whitespace runs — collapsed to a single space
It does not build an AST and makes no attempt to handle every edge case. It is designed to produce readable output for the 95% of real-world functions that are minified but structurally straightforward.
On Android where the popup fills the full screen, dropdown menus (Export, Settings) are positioned using position: fixed with JavaScript-calculated left and top values. The algorithm aligns menus based on where the trigger button sits in the viewport: buttons in the left half of the screen get left-aligned menus; buttons in the right half get right-aligned menus. Both edges are then clamped within the viewport with an 8px margin, so menus never go off-screen regardless of popup size or device width.
The author name, version string, and GitHub URL displayed in the About panel are hardcoded constants at the very top of popup.js. To change them, open popup.js and edit lines 5–7:
var ABOUT_AUTHOR = 'gerbil';
var ABOUT_GITHUB = 'https://github.com/gerbilbyte/VarScope';
var ABOUT_VERSION = 'v1.1.0';After saving, reload the extension in chrome://extensions (click the refresh icon on the VarScope card) for the changes to take effect.
PolyForm Noncommercial License 1.0.0
VarScope may be used freely for non-commercial purposes including personal use, security research, penetration testing engagements, and educational work. Commercial use — including use within a commercial product or as part of a paid service — requires a separate licence agreement.
Issues and pull requests are welcome.
Before submitting a pull request for a significant change, please open an issue first to discuss the approach. Bug fixes and documentation improvements can be submitted directly.
If you find a website or application where VarScope misclassifies a native variable as a webapp variable (or vice versa), please open an issue with the variable name and a link to the page — expanding the native key blocklist is an ongoing effort.