5 Commits

Author SHA1 Message Date
moamen 998c9d314f ci: add release + validation workflows
- release.yml: on v* tag, build Chrome + Firefox zips, compute
  SHA256, upload artifacts, attach to GitHub Release with auto
  generated notes
- ci.yml: on PR/master push, validate manifest JSON, enforce
  version parity, build, web-ext lint Firefox bundle
2026-05-15 00:48:49 +03:00
moamen e324d69611 Sync Firefox manifest with v2.1.0 hardening and include loader
- Bump Firefox manifest to 2.1.0 alongside Chrome
- Switch Firefox interceptor injection to loader pattern so the nonce
  handoff to content.js works (MAIN-world content script alone has no
  documentElement nonce, causing all findings to be rejected)
- Add CSP and web_accessible_resources entries to match Chrome
- Include js/interceptor-loader.js in build.sh shared files
- Ignore dist/ build output
2026-05-15 00:48:18 +03:00
anthonyonazure 6742d6a827 Reduce false positives in DOM, storage, URL, and keyword scanners
- Skip known CSRF tokens (authenticity_token, csrf_token, etc.) in hidden input scanner
- Ignore GitHub localStorage caches (ref-selector:*, jump_to:*, soft-nav:*, COPILOT_*)
- Skip keyboard shortcut data-attributes (data-hotkey, data-hotkey-scope)
- Fix URL param scanner: use exact match instead of substring to prevent "author" matching "auth"
- Add word boundaries to keyword scanner so "key" doesn't match "hotkey", "monkey", etc.
- Skip camelCase JS identifiers in keyword value matches
- Lower Sentry DSN severity to "low" (public by design)
- Apply same fixes to MutationObserver for SPA consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 00:47:12 +03:00
anthonyonazure 0d5ca42faa Bump version to 2.1.0 2026-05-15 00:47:12 +03:00
anthonyonazure bfc73ba018 Security hardening, bug fixes, and per-tab alert icon
- Prevent fake finding injection via per-session nonce validation between
  MAIN world interceptor and ISOLATED world content script
- Fix CSV formula injection in export by sanitizing cell values
- Serialize storage writes to prevent race conditions across tabs
- Cap findings at 5000 with oldest-first eviction
- Delete findings by unique ID instead of URL to avoid collateral removal
- Validate keyword length (50 chars) and count (50 max)
- Add MutationObserver for SPA support (dynamic DOM scanning)
- Add explicit CSP to manifest
- Add per-tab alert icon with red dot overlay when secrets are found
2026-05-15 00:47:00 +03:00
13 changed files with 29 additions and 190 deletions
-10
View File
@@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
groups:
actions-minor:
update-types: ["minor", "patch"]
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: Validate manifest JSON
run: |
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}
-58
View File
@@ -1,58 +0,0 @@
# Changelog
All notable changes to KeyFinder are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [SemVer](https://semver.org/spec/v2.0.0.html).
## [2.1.1] - 2026-05-14
### Added
- `SECURITY.md` with threat model, disclosure policy, and known limitations of the MAIN <-> ISOLATED nonce bridge
- `.github/dependabot.yml` for weekly GitHub Actions version bumps
- `CHANGELOG.md`
### Changed
- CSV export sanitiser now also prefixes cells starting with LF (`\n`), not just `=`, `+`, `-`, `@`, tab, CR
- Popup and results page version label is now read from the manifest at runtime instead of being hardcoded
### Fixed
- Window-global scan in `js/interceptor.js` now runs at `document_start`, `DOMContentLoaded`, and `load`, with per-name dedupe. The previous implementation only scanned at `document_start` when page globals had not yet been assigned, making the entire pass dead code on most real pages
## [2.1.0] - 2026-04-14
### Added
- Per-session nonce validation between MAIN-world interceptor and ISOLATED content script to prevent forged finding injection
- CSV formula-injection sanitiser on findings export
- Serialised storage writes to eliminate cross-tab race conditions
- 5000-finding cap with FIFO eviction
- Per-tab alert badge with red-dot icon overlay when secrets are detected
- MutationObserver scans dynamically-injected DOM nodes for SPA coverage
- Explicit Content Security Policy in Chrome and Firefox manifests
- `js/interceptor-loader.js` for both browsers, replacing direct MAIN-world content script on Firefox so the nonce handoff actually works
- GitHub Actions release pipeline (`.github/workflows/release.yml`): on `v*` tag, build Chrome + Firefox zips, compute SHA256, attach to GitHub Release
- GitHub Actions CI pipeline (`.github/workflows/ci.yml`): manifest JSON validation, Chrome <-> Firefox version parity check, build verification, `web-ext lint` on the Firefox bundle
### Changed
- Keyword input validation: 50 character maximum, 50 keyword maximum
- Findings are now deleted by unique ID instead of URL substring match
- URL parameter scanner uses exact match instead of substring (was matching `author` as `auth`)
- Keyword scanner enforces word boundaries (was matching `key` inside `hotkey`, `monkey`)
- camelCase JS identifiers are now skipped in keyword value matches
- Sentry DSN downgraded from `high` to `low` severity (public by design)
### Fixed
- Stored finding race conditions across concurrent tabs
- False positives from GitHub localStorage caches (`ref-selector:*`, `jump_to:*`, `soft-nav:*`, `COPILOT_*`)
- False positives from common CSRF tokens (`authenticity_token`, `csrf_token`, `__RequestVerificationToken`)
- False positives from keyboard shortcut data attributes (`data-hotkey`, `data-hotkey-scope`)
## [2.0.0] - 2026-04-07
### Added
- Complete rewrite to Manifest V3
- Enterprise-grade secret detection with 80+ regex patterns covering AWS, GCP, Azure, GitHub, GitLab, Stripe, PayPal, Square, Slack, Discord, and more
- Firefox support (MV3, Firefox 128+)
- Privacy policy
- Replaced demo gifs with professional logo
### Removed
- Manifest V2 background page
- Legacy jQuery dependency
+5 -21
View File
@@ -60,13 +60,10 @@ Additionally, **Shannon entropy analysis** is applied to detect random high-entr
- **Zero dependencies** - Pure vanilla JavaScript, no jQuery, no external libraries
- **Manifest V3** - Built for modern Chrome and Firefox with service worker architecture
- **Passive scanning** - Runs automatically on every page load
- **SPA-aware** - MutationObserver re-scans dynamically injected DOM
- **Per-tab alert badge** - Red-dot icon overlay when a tab has findings
- **Custom keywords** - Add your own search terms to scan for (validated, 50 max)
- **Custom keywords** - Add your own search terms to scan for
- **Dashboard** - Professional results page with filtering, sorting, and search
- **Export** - Download findings as JSON or CSV (with formula-injection sanitiser)
- **Hardened bridge** - Per-page nonce on MAIN <-> ISOLATED CustomEvent channel
- **Bounded storage** - 5000-finding cap with FIFO eviction; serialised writes across tabs
- **Export** - Download findings as JSON or CSV
- **Badge counter** - Shows finding count on the extension icon
- **Low footprint** - Minimal CPU and memory usage
- **All frames** - Scans iframes and embedded content
@@ -120,9 +117,8 @@ keyFinder/
js/
background.js # Service worker - storage and message handling
patterns.js # 80+ secret detection regex patterns
content.js # ISOLATED-world page scanner - DOM, scripts, network
interceptor-loader.js # ISOLATED loader - sets nonce, injects MAIN-world interceptor
interceptor.js # MAIN-world XHR/Fetch hooks + window global scanning
content.js # Page scanner - DOM, scripts, network interception
interceptor.js # XHR/Fetch hooking and window global scanning
popup.js # Popup logic
results.js # Dashboard logic with filtering and export
css/
@@ -136,22 +132,10 @@ keyFinder/
build.sh # Build Chrome and Firefox zip packages
```
## Security
See [SECURITY.md](SECURITY.md) for the threat model, disclosure policy, and known limitations. Release notes are in [CHANGELOG.md](CHANGELOG.md).
## Disclaimer
This tool is intended for **security research and authorized testing only**. Use it to identify leaked secrets on your own applications or during authorized penetration tests. You are responsible for your own actions.
## Professional services
keyFinder is built and maintained by [GreyCore Labs](https://greycorelabs.com), a US-incorporated offensive security firm. Want the same eye on your own product?
- [Penetration testing](https://greycorelabs.com/#plans) - web, API, mobile, cloud. Fixed quote within 24 hours, redacted sample report on request.
- [Free external attack-surface scan](https://greycorelabs.com/#free-scan) - one-page report in 48 hours, no strings attached.
## License
MIT
-57
View File
@@ -1,57 +0,0 @@
# Security Policy
## Reporting a vulnerability
Email **security reports** privately to the address on the maintainer's GitHub profile.
Do **not** open a public issue for unpatched vulnerabilities.
When reporting, include:
- Affected version (`manifest.json` `version` field)
- Browser and version
- Steps to reproduce
- Impact assessment (what an attacker gains)
A response is targeted within 7 days.
## Threat model
KeyFinder runs as a content script in every page the user visits and reports findings to a service worker. It is **client-side, passive, and read-only** with respect to the page.
### In scope
- Privilege escalation from a malicious page into the extension's service worker
- Persistent storage poisoning via crafted findings
- Cross-tab data leakage through `chrome.storage`
- CSV / JSON export injection (formulas, embedded HTML, JS)
- Manifest / CSP weaknesses enabling code injection into the extension's own pages
- Pattern-rule false positives that consistently leak benign data into findings
### Out of scope
- A malicious page generating **fake findings** in the user's results view. The MAIN-world interceptor and the ISOLATED content script communicate over `CustomEvent` with a per-page nonce stored as a `data-kf-verify` attribute on `documentElement`. The nonce is removed once the content script consumes it, but a page script that runs between `document_start` and `document_idle` can read the attribute and forge events. Mitigation cost is high (Symbols don't cross realms; postMessage is also page-visible). The impact is limited to **showing the user a finding that isn't real** - no data is exfiltrated, no privileged API is reached. Treat findings on a hostile page as advisory.
- Detection accuracy of individual regex rules. False positives and false negatives are expected; report a tuning issue rather than a CVE.
- Extension being uninstalled or disabled by the user.
## What the extension can see
| Surface | Scope |
|---|---|
| Page DOM | All pages (`<all_urls>`) at `document_idle` (ISOLATED world) and `document_start` (MAIN world interceptor) |
| Network | `fetch` and `XMLHttpRequest` responses initiated by the page (response bodies up to 500 KB are scanned) |
| Web storage | `localStorage`, `sessionStorage`, `document.cookie` on the page |
| Inter-extension | None. No host permissions beyond `activeTab` and `storage` |
The extension makes **no outbound network requests** other than fetching same-origin scripts already referenced by the page.
## Known limitations
- **Per-tab badge dot** is set on the first finding only; subsequent findings update the count but not the icon
- **5000 finding cap** with FIFO eviction. High-volume scans (heavy SPAs over a long session) will drop oldest findings
- **CSV export** prefixes a single-quote on cells starting with `=`, `+`, `-`, `@`, tab, or carriage return to neutralise Excel / Sheets formula injection. Line-feed prefix is not currently neutralised
- **Service worker restarts** drop the in-flight `storageQueue` Promise chain. Subsequent storage writes are still serialised; only pending writes from the killed worker are lost
## Supported versions
| Version | Supported |
|---|---|
| 2.1.x | yes |
| 2.0.x | no |
| < 2.0 | no |
+17 -33
View File
@@ -39,39 +39,23 @@
"__ENV__", "__CONFIG__", "ENV", "CONFIG",
];
const scannedGlobals = new Set();
function scanGlobals() {
for (const name of globalNames) {
if (scannedGlobals.has(name)) continue;
try {
const val = window[name];
if (val === undefined || val === null) continue;
const str = typeof val === "object" ? JSON.stringify(val) : String(val);
if (str.length < 8 || str === "[object Object]") continue;
scannedGlobals.add(name);
emit({
match: `window.${name}=${str.substring(0, 200)}`,
type: "window-global",
patternName: "Exposed Global Variable",
severity: "high",
confidence: typeof val !== "object" && isHighEntropy(str.substring(0, 60)) ? "high" : "medium",
provider: "JS Global Scan",
isObject: typeof val === "object",
rawText: typeof val === "object" ? str.substring(0, 5000) : null,
});
} catch {}
}
}
// Run at document_start (likely empty), DOMContentLoaded, and load to catch
// globals set at any phase. Each name reports at most once via scannedGlobals.
scanGlobals();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", scanGlobals, { once: true });
}
if (document.readyState !== "complete") {
window.addEventListener("load", scanGlobals, { once: true });
} else {
scanGlobals();
for (const name of globalNames) {
try {
const val = window[name];
if (val === undefined || val === null) continue;
const str = typeof val === "object" ? JSON.stringify(val) : String(val);
if (str.length < 8 || str === "[object Object]") continue;
emit({
match: `window.${name}=${str.substring(0, 200)}`,
type: "window-global",
patternName: "Exposed Global Variable",
severity: "high",
confidence: typeof val !== "object" && isHighEntropy(str.substring(0, 60)) ? "high" : "medium",
provider: "JS Global Scan",
isObject: typeof val === "object",
rawText: typeof val === "object" ? str.substring(0, 5000) : null,
});
} catch {}
}
const origXhrOpen = XMLHttpRequest.prototype.open;
-2
View File
@@ -1,8 +1,6 @@
document.addEventListener("DOMContentLoaded", init);
async function init() {
const versionLabel = document.getElementById("versionLabel");
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
await renderKeywords();
await renderStats();
document.getElementById("keywordForm").addEventListener("submit", handleAddKeyword);
+1 -3
View File
@@ -3,8 +3,6 @@ let allFindings = [];
document.addEventListener("DOMContentLoaded", init);
async function init() {
const versionLabel = document.getElementById("versionLabel");
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
allFindings = response.findings || [];
@@ -216,7 +214,7 @@ function exportJson() {
function csvSafe(value) {
let str = String(value || "");
if (/^[=+\-@\t\r\n]/.test(str)) str = "'" + str;
if (/^[=+\-@\t\r]/.test(str)) str = "'" + str;
str = str.replace(/"/g, '""');
return `"${str}"`;
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "KeyFinder",
"description": "Passively discovers API keys, tokens, and secrets leaked in page scripts, DOM, network responses, and browser storage. Available for Chrome and Firefox.",
"version": "2.1.1",
"version": "2.1.0",
"manifest_version": 3,
"browser_specific_settings": {
"gecko": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "KeyFinder",
"description": "Passively discovers API keys, tokens, and secrets leaked in page scripts, DOM, network responses, and browser storage. Available for Chrome and Firefox.",
"version": "2.1.1",
"version": "2.1.0",
"manifest_version": 3,
"action": {
"default_icon": {
+1 -1
View File
@@ -11,7 +11,7 @@
<div class="header-brand">
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
<h1>KeyFinder</h1>
<span class="version" id="versionLabel"></span>
<span class="version">v2.0</span>
</div>
<p class="header-tagline">Passive API key & secret discovery</p>
</header>
+1 -1
View File
@@ -10,7 +10,7 @@
<header class="header">
<div class="header-left">
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
<h1>KeyFinder <span class="version" id="versionLabel"></span></h1>
<h1>KeyFinder <span class="version">v2.0</span></h1>
</div>
<div class="header-actions">
<div class="filter-group">