Building a Browser Extension to Hunt Exposed API Keys: A Deep Dive into KeyFinder's Architecture
Hook
Every day, developers accidentally commit over 10,000 secrets to public repositories. But an even larger attack surface exists in the browser itself—inline scripts, localStorage, and API responses where credentials leak in plain sight during normal web browsing.
Context
The modern web application is a secret-leaking machine. Between frontend JavaScript bundles containing hardcoded API keys, localStorage persisting OAuth tokens, and AJAX responses accidentally echoing AWS credentials, the browser has become a goldmine for security researchers. Traditional secret scanners like TruffleHog and Gitleaks focus on git repositories and filesystems, but they miss an entire class of exposure: secrets that only exist in the browser runtime.
Bug bounty hunters and penetration testers have long known that simply browsing a target application while monitoring DevTools can reveal surprising leaks. The problem is manual—opening the Network tab, checking Sources for suspicious strings, inspecting localStorage, reading through minified JavaScript. KeyFinder automates this reconnaissance by continuously scanning every page you visit across 10 distinct attack surfaces, from obvious vectors like inline scripts to subtle ones like HTML data attributes and meta tags. It's passive reconnaissance as a browser extension, designed for the zero-config workflow security researchers actually need.
Technical Insight
KeyFinder's architecture centers on three components working in concert: a service worker for state management, content scripts injected into every page, and an interceptor script that hooks into native browser APIs. This design is dictated by Manifest V3's security model, which removed background pages in favor of ephemeral service workers and tightened content security policies.
The core scanning logic lives in content.js, injected into every page via manifest permissions. It scans the DOM systematically across multiple surfaces. Here's a simplified version of how it detects secrets in inline scripts:
function scanInlineScripts() {
const scripts = document.querySelectorAll('script:not([src])');
const findings = [];
scripts.forEach(script => {
const content = script.textContent;
// Pattern matching for known secret formats
PATTERNS.forEach(pattern => {
const matches = content.match(pattern.regex);
if (matches) {
matches.forEach(match => {
findings.push({
type: pattern.type,
value: match,
surface: 'inline_script',
location: script.outerHTML.substring(0, 100)
});
});
}
});
// Shannon entropy analysis for high-entropy strings
const highEntropyStrings = extractHighEntropyStrings(content);
highEntropyStrings.forEach(str => {
findings.push({
type: 'high_entropy_secret',
value: str,
entropy: calculateEntropy(str),
surface: 'inline_script'
});
});
});
return findings;
}
function calculateEntropy(str) {
const freq = {};
for (let c of str) freq[c] = (freq[c] || 0) + 1;
return Object.values(freq).reduce((entropy, count) => {
const p = count / str.length;
return entropy - p * Math.log2(p);
}, 0);
}
The Shannon entropy calculation is crucial—it catches custom or undocumented secret formats that don't match any known pattern. Strings with entropy above 4.5 bits per character get flagged, which typically indicates random tokens or Base64-encoded data.
Network interception is where things get architecturally interesting. Manifest V3 doesn't allow direct modification of network requests from content scripts, so KeyFinder injects an interceptor that runs in the page context itself, hooking XMLHttpRequest and fetch before any application code executes:
// Injected into page context before other scripts
(function() {
const originalFetch = window.fetch;
const originalXHROpen = XMLHttpRequest.prototype.open;
window.fetch = async function(...args) {
const response = await originalFetch.apply(this, args);
const clonedResponse = response.clone();
try {
const text = await clonedResponse.text();
window.postMessage({
type: 'KEYFINDER_RESPONSE',
url: args[0],
data: text
}, '*');
} catch (e) {}
return response;
};
XMLHttpRequest.prototype.open = function(method, url) {
this.addEventListener('load', function() {
if (this.responseType === '' || this.responseType === 'text') {
window.postMessage({
type: 'KEYFINDER_RESPONSE',
url: url,
data: this.responseText
}, '*');
}
});
return originalXHROpen.apply(this, arguments);
};
})();
This runs in the page's context with full access to native APIs before they're overridden by frameworks. The intercepted data flows back to the content script via postMessage, then to the service worker for aggregation. It's a clever workaround for Manifest V3's restrictions while maintaining the passive nature of the tool.
The 80+ detection patterns cover everything from AWS access keys (AKIA[0-9A-Z]{16}) to Slack tokens, Google API keys, and private keys. But the pattern library isn't just regex—it includes validation logic to reduce false positives. For example, the AWS key pattern checks that the subsequent characters follow AWS's actual key generation format, filtering out strings that merely look similar.
Results aggregate in background.js (the service worker), which maintains state across page loads and provides a centralized store. The dashboard UI queries this store and offers filtering by secret type, surface, and domain, plus JSON/CSV export for integration with bug bounty reports. This separation of concerns—scanning in content scripts, state in the service worker, presentation in the popup—is the clean architecture Manifest V3 encourages.
Gotcha
KeyFinder's passive nature is both its strength and limitation. It only sees what the browser sees, which means secrets in server-side code, database queries, or unauthenticated API endpoints remain invisible. If your target application uses server-side rendering and keeps secrets server-side (as they should), KeyFinder won't help. It's a reconnaissance tool for client-side leaks, not a comprehensive secrets audit.
The regex-based detection, even with entropy analysis, generates false positives. Strings like const API_KEY = 'test_key_placeholder' will trigger alerts. Base64-encoded images can trigger high-entropy warnings. You'll need to manually validate findings, which means KeyFinder is a lead generator, not a definitive answer. For bug bounty work, this is fine—you're already validating everything—but for automated CI/CD integration, the false positive rate makes it impractical. Additionally, CORS restrictions limit external script scanning to same-origin resources, so secrets in third-party JavaScript hosted on CDNs might slip through unless those scripts are proxied through the target domain.
Verdict
Use KeyFinder if you're conducting security research, bug bounties, or penetration testing on web applications and want automated passive reconnaissance running in the background as you browse. It's perfect for identifying low-hanging fruit like exposed API keys in frontend code, localStorage tokens, or secrets accidentally logged in network responses. The zero-config, zero-dependency design means you install it and forget it—no configuration files, no API keys to manage. Skip KeyFinder if you need active scanning with authentication flows, server-side code analysis, or if you're building automated pipelines where false positives would create noise. This isn't a replacement for proper secrets management or CI/CD scanning tools like Gitleaks or GitGuardian. It's a specialized reconnaissance tool for the browser attack surface, and it excels in that niche.