mirror of
https://github.com/momenbasel/keyFinder.git
synced 2026-07-24 05:30:50 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b01208b00 | |||
| 5d606109dc | |||
| 50a2f8b482 | |||
| 72f324adae | |||
| 806e0a4a7d |
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
groups:
|
||||||
|
actions-minor:
|
||||||
|
update-types: ["minor", "patch"]
|
||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
validate:
|
validate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Validate manifest JSON
|
- name: Validate manifest JSON
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# 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
|
||||||
@@ -60,10 +60,13 @@ Additionally, **Shannon entropy analysis** is applied to detect random high-entr
|
|||||||
- **Zero dependencies** - Pure vanilla JavaScript, no jQuery, no external libraries
|
- **Zero dependencies** - Pure vanilla JavaScript, no jQuery, no external libraries
|
||||||
- **Manifest V3** - Built for modern Chrome and Firefox with service worker architecture
|
- **Manifest V3** - Built for modern Chrome and Firefox with service worker architecture
|
||||||
- **Passive scanning** - Runs automatically on every page load
|
- **Passive scanning** - Runs automatically on every page load
|
||||||
- **Custom keywords** - Add your own search terms to scan for
|
- **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)
|
||||||
- **Dashboard** - Professional results page with filtering, sorting, and search
|
- **Dashboard** - Professional results page with filtering, sorting, and search
|
||||||
- **Export** - Download findings as JSON or CSV
|
- **Export** - Download findings as JSON or CSV (with formula-injection sanitiser)
|
||||||
- **Badge counter** - Shows finding count on the extension icon
|
- **Hardened bridge** - Per-page nonce on MAIN <-> ISOLATED CustomEvent channel
|
||||||
|
- **Bounded storage** - 5000-finding cap with FIFO eviction; serialised writes across tabs
|
||||||
- **Low footprint** - Minimal CPU and memory usage
|
- **Low footprint** - Minimal CPU and memory usage
|
||||||
- **All frames** - Scans iframes and embedded content
|
- **All frames** - Scans iframes and embedded content
|
||||||
|
|
||||||
@@ -117,8 +120,9 @@ keyFinder/
|
|||||||
js/
|
js/
|
||||||
background.js # Service worker - storage and message handling
|
background.js # Service worker - storage and message handling
|
||||||
patterns.js # 80+ secret detection regex patterns
|
patterns.js # 80+ secret detection regex patterns
|
||||||
content.js # Page scanner - DOM, scripts, network interception
|
content.js # ISOLATED-world page scanner - DOM, scripts, network
|
||||||
interceptor.js # XHR/Fetch hooking and window global scanning
|
interceptor-loader.js # ISOLATED loader - sets nonce, injects MAIN-world interceptor
|
||||||
|
interceptor.js # MAIN-world XHR/Fetch hooks + window global scanning
|
||||||
popup.js # Popup logic
|
popup.js # Popup logic
|
||||||
results.js # Dashboard logic with filtering and export
|
results.js # Dashboard logic with filtering and export
|
||||||
css/
|
css/
|
||||||
@@ -132,10 +136,22 @@ keyFinder/
|
|||||||
build.sh # Build Chrome and Firefox zip packages
|
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
|
## 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.
|
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
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
# 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 |
|
||||||
+33
-17
@@ -39,23 +39,39 @@
|
|||||||
"__ENV__", "__CONFIG__", "ENV", "CONFIG",
|
"__ENV__", "__CONFIG__", "ENV", "CONFIG",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const name of globalNames) {
|
const scannedGlobals = new Set();
|
||||||
try {
|
function scanGlobals() {
|
||||||
const val = window[name];
|
for (const name of globalNames) {
|
||||||
if (val === undefined || val === null) continue;
|
if (scannedGlobals.has(name)) continue;
|
||||||
const str = typeof val === "object" ? JSON.stringify(val) : String(val);
|
try {
|
||||||
if (str.length < 8 || str === "[object Object]") continue;
|
const val = window[name];
|
||||||
emit({
|
if (val === undefined || val === null) continue;
|
||||||
match: `window.${name}=${str.substring(0, 200)}`,
|
const str = typeof val === "object" ? JSON.stringify(val) : String(val);
|
||||||
type: "window-global",
|
if (str.length < 8 || str === "[object Object]") continue;
|
||||||
patternName: "Exposed Global Variable",
|
scannedGlobals.add(name);
|
||||||
severity: "high",
|
emit({
|
||||||
confidence: typeof val !== "object" && isHighEntropy(str.substring(0, 60)) ? "high" : "medium",
|
match: `window.${name}=${str.substring(0, 200)}`,
|
||||||
provider: "JS Global Scan",
|
type: "window-global",
|
||||||
isObject: typeof val === "object",
|
patternName: "Exposed Global Variable",
|
||||||
rawText: typeof val === "object" ? str.substring(0, 5000) : null,
|
severity: "high",
|
||||||
});
|
confidence: typeof val !== "object" && isHighEntropy(str.substring(0, 60)) ? "high" : "medium",
|
||||||
} catch {}
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
const origXhrOpen = XMLHttpRequest.prototype.open;
|
const origXhrOpen = XMLHttpRequest.prototype.open;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
document.addEventListener("DOMContentLoaded", init);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
const versionLabel = document.getElementById("versionLabel");
|
||||||
|
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
|
||||||
await renderKeywords();
|
await renderKeywords();
|
||||||
await renderStats();
|
await renderStats();
|
||||||
document.getElementById("keywordForm").addEventListener("submit", handleAddKeyword);
|
document.getElementById("keywordForm").addEventListener("submit", handleAddKeyword);
|
||||||
|
|||||||
+3
-1
@@ -3,6 +3,8 @@ let allFindings = [];
|
|||||||
document.addEventListener("DOMContentLoaded", init);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
async function 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" });
|
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
||||||
allFindings = response.findings || [];
|
allFindings = response.findings || [];
|
||||||
|
|
||||||
@@ -214,7 +216,7 @@ function exportJson() {
|
|||||||
|
|
||||||
function csvSafe(value) {
|
function csvSafe(value) {
|
||||||
let str = String(value || "");
|
let str = String(value || "");
|
||||||
if (/^[=+\-@\t\r]/.test(str)) str = "'" + str;
|
if (/^[=+\-@\t\r\n]/.test(str)) str = "'" + str;
|
||||||
str = str.replace(/"/g, '""');
|
str = str.replace(/"/g, '""');
|
||||||
return `"${str}"`;
|
return `"${str}"`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "KeyFinder",
|
"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.",
|
"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.0",
|
"version": "2.1.1",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "KeyFinder",
|
"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.",
|
"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.0",
|
"version": "2.1.1",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"action": {
|
"action": {
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
<div class="header-brand">
|
<div class="header-brand">
|
||||||
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
||||||
<h1>KeyFinder</h1>
|
<h1>KeyFinder</h1>
|
||||||
<span class="version">v2.0</span>
|
<span class="version" id="versionLabel"></span>
|
||||||
</div>
|
</div>
|
||||||
<p class="header-tagline">Passive API key & secret discovery</p>
|
<p class="header-tagline">Passive API key & secret discovery</p>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
||||||
<h1>KeyFinder <span class="version">v2.0</span></h1>
|
<h1>KeyFinder <span class="version" id="versionLabel"></span></h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user