When Static Analysis Fails: Instrumenting the Browser to Spot Token Theft

When Static Analysis Fails: Instrumenting the Browser to Spot Token Theft

pr0h0
cybersecuritybrowser-securitystatic-analysistoken-theft
AI Usage (81%)

Why this report matters

The core claim from the source: the attack flow moves into the browser

What I took from the “EvilTokens” report is not the headline label. It is the operational shift: part of the attack logic only becomes visible once the page is running in a browser. The source says static analysis misses that browser-side behavior.

That matters because a lot of reviews still stop at the bundle. People unpack JavaScript, grep for suspicious strings, scan dependencies, and call it done. That works for some abuse patterns. It breaks when the interesting behavior appears only after the page loads, handlers attach, storage is read, and requests are built at runtime.

Why static analysis alone misses this flow

Static analysis is still useful, but it has a ceiling: it can show you what code exists, not what the page actually does in a live session.

If a malicious or compromised page:

  • waits for a user gesture,
  • reads state from storage or the DOM,
  • builds requests through wrappers or callbacks,
  • routes data through timers or async branches,

then the key evidence may never appear as a simple string match in the bundle. That is why browser instrumentation is the better control for this kind of investigation.

What static analysis can and cannot see

Build-time artifacts versus runtime behavior

Static analysis sees the bundle as shipped:

  • minified source
  • dependency graph
  • imported modules
  • string literals
  • obvious sink calls such as fetch() or XMLHttpRequest

It does not reliably see:

  • branches that depend on live DOM state
  • values read from storage at runtime
  • requests assembled after obfuscation or deferred execution
  • code generated by page logic, not the build system

So static analysis tells you what might happen. Runtime observation tells you what did happen.

Common blind spots in bundled JavaScript and page-driven logic

The blind spots are usually ordinary things, which is why they get missed:

  • event listeners attached after hydration
  • conditional request logic hidden behind feature flags
  • obfuscated helpers that only resolve in the browser
  • third-party scripts that mutate the page after load
  • data passed through closures instead of obvious globals

On a large bundle, a reviewer can also lose the thread because the “token” never appears next to the exfiltration sink. It may be read in one file, transformed in another, and sent in a third.

Why token theft often hides in event handlers, DOM state, and fetch wrappers

Token theft rarely needs exotic primitives. It often uses normal browser behavior against the app:

  • localStorage or sessionStorage reads
  • hidden form fields or meta tags
  • request wrappers that add headers
  • DOM mutations that trigger the next step
  • redirects that move a token-bearing URL or fragment somewhere else

That is why I would not trust a static scan that only asks, “Does this bundle contain suspicious exfiltration code?” The better question is, “What does the page do after it runs?”

Instrumenting the browser instead of trusting the bundle

Hooking fetch, XMLHttpRequest, storage reads, and navigation changes

For a safe lab page, I start by instrumenting the browser at runtime and logging the behavior I care about. The point is not to break the app. The point is to observe it.

Here is a small DevTools-style hook set you can paste into a throwaway test page:

(() => {
  const originalFetch = window.fetch;
  window.fetch = async (...args) => {
    console.log("[fetch]", args[0], args[1] || {});
    return originalFetch(...args);
  };

  const originalOpen = XMLHttpRequest.prototype.open;
  const originalSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.open = function (method, url, ...rest) {
    this.__url = url;
    this.__method = method;
    return originalOpen.call(this, method, url, ...rest);
  };

  XMLHttpRequest.prototype.send = function (body) {
    console.log("[xhr]", this.__method, this.__url, body || null);
    return originalSend.call(this, body);
  };

  const originalGetItem = Storage.prototype.getItem;
  Storage.prototype.getItem = function (key) {
    const value = originalGetItem.call(this, key);
    console.log("[storage:getItem]", key, value);
    return value;
  };

  const originalSetItem = Storage.prototype.setItem;
  Storage.prototype.setItem = function (key, value) {
    console.log("[storage:setItem]", key, value);
    return originalSetItem.call(this, key, value);
  };

  const originalAssign = window.location.assign.bind(window.location);
  window.location.assign = function (url) {
    console.log("[nav:assign]", url);
    return originalAssign(url);
  };
})();

This does not prove theft by itself. It gives you runtime evidence when the page touches credential-bearing paths.

Watching DOM mutations and credential-bearing requests

If the page pulls data from the DOM, storage hooks are not enough. Add a mutation observer and a request filter:

const observer = new MutationObserver((records) => {
  for (const record of records) {
    for (const node of record.addedNodes) {
      if (node.nodeType === 1) {
        console.log("[dom:add]", node.tagName, node.outerHTML?.slice(0, 200));
      }
    }
  }
});

observer.observe(document.documentElement, {
  childList: true,
  subtree: true,
});

Then watch for requests that carry likely credentials:

  • Authorization headers
  • cookies on same-site requests
  • query parameters that look like session values
  • POST bodies containing tokens or one-time codes

The safest pattern is to log only what you need for verification, not a full dump of user data.

Capturing runtime evidence without changing the page's logic

A good investigation avoids changing the page’s decision-making. I want visibility, not interference.

That means:

  • observe before action
  • log request shape, destination, and timing
  • avoid monkey-patching application business logic
  • keep the test environment isolated from real accounts

If the page behaves differently under heavy instrumentation, call that out as a limitation. Do not pretend the hooks were neutral if they may have altered the flow.

A safe lab workflow for reproducing the pattern

Set up a throwaway browser profile and isolated test page

Use a clean browser profile and a local test page that mimics the structure of the suspected app. I usually recommend:

  1. a fresh Chrome or Firefox profile
  2. a local static page or disposable test app
  3. fake tokens only, never production credentials
  4. a local HTTP listener that records requests

For a minimal receiver, you can use a tiny Node server:

http
  .createServer((req, res) => {
    let body = "";
    req.on("data", (chunk) => (body += chunk));
    req.on("end", () => {
      console.log(req.method, req.url);
      console.log("headers:", req.headers);
      console.log("body:", body);
      res.writeHead(204);
      res.end();
    });
  })
  .listen(3000, () => console.log("listening on http://127.0.0.1:3000"));

Trace requests, console output, and storage access step by step

In the browser, load the page, trigger one user action at a time, and record what happens.

A useful capture sequence looks like this:

[storage:getItem] access_token fake-lab-token-123
[fetch] http://127.0.0.1:3000/collect {
  method: "POST",
  headers: { "content-type": "application/json" }
}
[xhr] POST /api/session/refresh null
[nav:assign] https://example.test/logout

That is the kind of transcript you want in a report. It puts the claim and the observed signal side by side.

Record the observed signal next to each claim, not just the suspicion

I would separate notes like this:

ClaimObserved signalConfidence
Page reads a token from storage[storage:getItem] access_token ... appears before the requestConfirmed
Page sends token off-originfetch target is a different hostConfirmed
Intent is malicious theftNeeds context from the full flow and source write-upInference

That table forces discipline. It keeps confirmed behavior separate from interpretation.

What token theft looks like at runtime

How a page can read tokens from memory, storage, or hidden inputs

At runtime, the token can come from several places:

  • localStorage or sessionStorage
  • JavaScript variables that live in memory after login
  • hidden inputs rendered into the DOM
  • meta tags or bootstrap JSON
  • cookies if the app exposes them to script

The browser does not care whether the code is “legitimate” application code or malicious script. If the origin can read the value, runtime code can move it.

How the same flow can be split across timers, callbacks, and obfuscated branches

A common trick is to split the flow:

  1. read state in one callback
  2. wait on a timer
  3. transform the token
  4. send it only after a seemingly unrelated event

Static analysis often struggles here because no single function contains the full story. The code looks like harmless event wiring until you trace the runtime sequence.

The difference between confirmed behavior and likely intent

This distinction matters.

Confirmed:

  • a page read a value
  • a request was sent
  • the destination and payload shape are visible

Likely, but not yet confirmed:

  • the request was intended as exfiltration
  • the token was the primary target
  • obfuscation was used to hide the behavior from reviewers

If you cannot prove intent, say so. Strong reports hold up because they do not overstate the evidence.

Defensive checks developers should add

Do not store long-lived tokens where browser scripts can read them unless you have a strong reason

If script-accessible storage is not necessary, do not use it for long-lived credentials. That is still my default position.

Local browser storage is convenient, but convenience is not a security argument. If a script can run in your origin, it can usually read what the origin stores.

Prefer short-lived credentials, scoped tokens, and backend authorization checks

Better controls are boring and effective:

  • short-lived access tokens
  • refresh flows with tighter server checks
  • scopes that limit what a token can do
  • backend authorization on every sensitive action

The real fix belongs on the server. Client-side checks can improve UX, but they do not enforce trust boundaries.

Add CSP, dependency review, and runtime monitoring as layered controls

No single control solves this class of issue.

Use:

  • a restrictive Content Security Policy
  • dependency review for third-party scripts
  • subresource integrity where practical
  • runtime monitoring for unusual request destinations
  • alerting on storage reads plus outbound calls in sensitive flows

CSP will not stop every abuse path, but it can narrow the blast radius if a script gets in.

What security teams should look for during investigation

Network destinations, request timing, and suspicious token exfiltration patterns

The first thing I inspect is the network graph:

  • off-origin requests soon after login
  • requests that happen only after a specific UI event
  • repeated calls to the same unknown endpoint
  • encoded or compressed request bodies that are hard to inspect

Timing matters. A token read followed immediately by a cross-origin POST is a stronger signal than either event alone.

Unexpected reads from localStorage, sessionStorage, cookies, or injected form fields

If you have browser telemetry, look for:

  • storage access before outbound requests
  • reads from keys that should be app-internal
  • DOM access to hidden fields or bootstrap blobs
  • scripts touching values unrelated to their UI role

A normal page may read storage. A suspicious page reads storage and immediately ships the result somewhere else.

Browser-side telemetry that helps distinguish user action from script-driven theft

Useful telemetry includes:

  • event source and timing
  • request initiator stack traces when available
  • destination domains
  • whether a request was tied to a real user gesture
  • whether the same path runs on page load without interaction

That last one matters. Script-driven theft often tries to blend in with normal app traffic.

What I confirmed and what I did not

Confirmed from the source material: the browser hides the attack flow and static analysis misses part of it

From the supplied source context, the confirmed point is narrow but useful: the report says the attack flow is in the browser and that static analysis leaves gaps. That is enough to justify runtime inspection as the next step.

Not confirmed by the provided source: exact payload chain, target applications, and specific token types

I did not receive enough primary material to confirm:

  • the full payload chain
  • the exact applications affected
  • which token types were targeted
  • whether the sample was a live incident, a demo, or a broader research finding

So I am not treating those details as facts here. They may exist in the full article, but I cannot infer them from the snippet alone.

Practical takeaway

Static analysis is useful, but runtime browser instrumentation is the control that catches this class of abuse

My position is simple: if the attack path moves into the browser, static analysis becomes a supporting tool, not the main one. Use it to narrow candidates. Use the browser to verify behavior.

Fix the backend trust boundary first, then use browser telemetry to verify the client path is not leaking credentials

If you only patch the bundle, you are probably treating the symptom. The actual fix belongs at the trust boundary:

  • reduce what the browser can see
  • reduce what it can send
  • verify sensitive actions server-side
  • instrument the runtime so you can prove the client path is clean

That is the practical lesson behind this report. When static analysis fails, the browser itself becomes the evidence source.

Further reading

Link to the original report or source article

Relevant browser security and web app guidance for token handling and CSP

Share this post

More posts

Comments