> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

Tracy: Browser-Based Taint Tracking That Bypasses the Proxy Tax

[ View on GitHub ]

Tracy: Browser-Based Taint Tracking That Bypasses the Proxy Tax

Hook

Most pentesters spend 15 minutes configuring proxies and certificates before testing even begins. Tracy eliminates this entire workflow by moving vulnerability reconnaissance directly into the browser where modern web applications actually execute.

Context

The shift from server-rendered pages to JavaScript-heavy single-page applications fundamentally broke traditional web security testing. Tools like Burp Suite and OWASP ZAP were architected for a simpler era: intercept HTTP requests at the network layer, fuzz parameters, observe responses. But when application logic moved into the browser—where React components transform data, client-side routers manage navigation, and DOM manipulation happens asynchronously—these proxy-based tools became partially blind.

DOM-based XSS vulnerabilities, where user input reaches dangerous JavaScript sinks like eval() or innerHTML without ever touching the server, are invisible to network-layer analysis. Meanwhile, tracing how input propagates through layers of JavaScript transformations requires either tedious manual inspection or complex dynamic analysis frameworks that most pentesters don't have time to configure. Tracy emerged from NCC Group's consulting work to address this gap: a lightweight browser extension that instruments web applications in real-time, tagging user inputs with unique identifiers and tracking them as they flow through client-side code to potential security sinks.

Technical Insight

Backend

Browser Context

Injects tagged input

Wraps with tracer IDs

DOM mutations

Network responses

Eval/innerHTML calls

Detects tracers in sinks

Stores observations

Original input

Browser Extension

Web Application

User Input

JavaScript Hooks

Local API Server

Analysis UI

System architecture — auto-generated

Tracy's architecture revolves around three components: a browser extension (supporting both Chrome and Firefox), a local API server that stores observations, and a web UI for analysis. The genius lies in its tagging strategy. When you interact with a web application—filling forms, modifying URL parameters, or triggering any input mechanism—Tracy's injected JavaScript wraps your input with uniquely identifiable strings.

Here's a simplified version of how Tracy generates tracers:

// Tracy injects code that wraps user input with unique identifiers
function generateTracer(input, tracerID) {
  // Uses non-printing Unicode characters as delimiters
  const delimiter = String.fromCharCode(0x200B); // Zero-width space
  return `${delimiter}{{${tracerID}}}${delimiter}${input}`;
}

// Example: User types "test" in a search field
// Tracy transforms it to: "​{{A1B2C3}}​test"
// The zero-width characters are invisible but detectable

The extension then monitors the execution environment for these tracers. It hooks into dangerous JavaScript sinks by overriding native browser APIs before page scripts execute:

// Tracy's content script runs before page JavaScript
const originalInnerHTML = Object.getOwnPropertyDescriptor(
  Element.prototype, 'innerHTML'
).set;

Object.defineProperty(Element.prototype, 'innerHTML', {
  set: function(value) {
    // Check if any tracers are present in the value
    const tracerPattern = /{{([A-Z0-9]+)}}/g;
    const matches = value.match(tracerPattern);
    
    if (matches) {
      // Send finding to API server
      reportSink({
        tracerIDs: matches.map(m => m.slice(2, -2)),
        sink: 'innerHTML',
        context: value,
        location: window.location.href,
        stackTrace: new Error().stack
      });
    }
    
    // Call original function
    return originalInnerHTML.call(this, value);
  }
});

This interception pattern extends to dozens of sinks: document.write(), eval(), setTimeout() with string arguments, various DOM manipulation methods, and even fetch/XHR requests. When Tracy detects a tracer reaching a sink, it captures the full context—including stack traces that show exactly which JavaScript functions processed the data.

The browser extension communicates with a local REST API server (written in Go) that persists findings to a SQLite database. This separation allows multiple browser sessions to share observations and provides a clean interface for querying results. The web UI displays a sortable table of all traced inputs, which sinks they reached, and the transformation path they took.

What makes Tracy particularly effective for modern SPAs is its ability to track data through asynchronous operations. Because the tracers persist as string content, they survive Promise chains, localStorage round-trips, and even WebSocket messages. If you submit {{ABC123}}admin in a form and it eventually appears in a component's dangerouslySetInnerHTML three navigation states later, Tracy catches it.

The extension also handles encoded data intelligently. If your input gets URL-encoded, base64-encoded, or JSON-stringified, Tracy recognizes the tracer patterns in their transformed states:

// Tracy can detect tracers through common encodings
const patterns = [
  /{{([A-Z0-9]+)}}/g,                    // Plain
  /%7B%7B([A-Z0-9]+)%7D%7D/g,           // URL-encoded
  /eyJ7W0EtWjAtOV0rfX0=/g,              // Base64 pattern
  /\\u007b\\u007b([A-Z0-9]+)/g         // Unicode-escaped
];

This multi-encoding detection dramatically reduces false negatives when applications transform input before rendering it.

Gotcha

Tracy's browser-only visibility creates blind spots that pentesters must understand. If your tagged input reaches the server and triggers a stored XSS vulnerability that affects other users, Tracy won't see the exploit—it only observes the current browser session. Server-side validation bypasses, authentication issues, or backend logic flaws are completely out of scope. You're essentially trading comprehensive coverage for deployment simplicity.

The tool also suffers from the manual exploration problem. Unlike automated scanners that crawl and fuzz endpoints systematically, Tracy only tracks what you actually interact with. Testing a large application requires methodically clicking through every feature, filling every form, and manipulating every parameter. This is time-consuming, and you might miss attack surface if you don't thoroughly explore the application. Additionally, modern web frameworks with aggressive sanitization or strict Content Security Policies might neutralize XSS attempts even when Tracy identifies a sink. The tool shows you where data flows but doesn't evaluate whether exploitation is actually feasible—you'll find yourself investigating many paths that lead to dead ends. Finally, applications that heavily obfuscate or minify their JavaScript can make stack traces difficult to interpret, though the sink detection itself still functions.

Verdict

Use Tracy if you're pentesting JavaScript-heavy single-page applications where traditional proxy-based tools struggle to track client-side data flow, especially when hunting DOM-based XSS vulnerabilities. It's invaluable during manual reconnaissance phases where you need to quickly map which user inputs reach dangerous sinks without spending time configuring proxies or dealing with certificate warnings on mobile/embedded browsers. The browser-native approach makes it perfect for fast-turnaround assessments where setup time matters. Skip Tracy if you need comprehensive automated scanning with active exploitation, are testing simple server-rendered applications where input/output relationships are obvious, or require detection of stored XSS, server-side vulnerabilities, or backend logic flaws. Don't use it as your only tool—it's reconnaissance intelligence that informs where you focus deeper manual testing, not a replacement for thorough security assessment workflows.