5 Commits

Author SHA1 Message Date
dependabot[bot] 7b01208b00 build(deps): bump actions/checkout from 4 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 14:43:48 +00:00
Moamen Basel 5d606109dc Add GreyCore Labs professional services section to README 2026-06-11 02:41:01 +03:00
moamen 50a2f8b482 Update README: reflect SPA support, nonce bridge, loader, SECURITY links 2026-05-15 01:28:47 +03:00
moamen 72f324adae v2.1.1: deferred globals scan + CSV LF + version label fix
See CHANGELOG.md for full notes. Tagged release will fire the
GH Actions release pipeline added in v2.1.0.
2026-05-15 01:27:53 +03:00
Moamen Basel 806e0a4a7d Post-merge hardening: CSV LF, version label, deferred globals, SECURITY.md, CHANGELOG, dependabot (#16)
Five follow-ups from auditing #15: CSV LF prefix, runtime version label, deferred window-global scan, SECURITY.md threat model, CHANGELOG.md, dependabot.
2026-05-15 01:27:20 +03:00
13 changed files with 190 additions and 29 deletions
+10
View File
@@ -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"]
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Validate manifest JSON
run: |
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.tag || github.ref }}
+58
View File
@@ -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
+21 -5
View File
@@ -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
- **Manifest V3** - Built for modern Chrome and Firefox with service worker architecture
- **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
- **Export** - Download findings as JSON or CSV
- **Badge counter** - Shows finding count on the extension icon
- **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
- **Low footprint** - Minimal CPU and memory usage
- **All frames** - Scans iframes and embedded content
@@ -117,8 +120,9 @@ keyFinder/
js/
background.js # Service worker - storage and message handling
patterns.js # 80+ secret detection regex patterns
content.js # Page scanner - DOM, scripts, network interception
interceptor.js # XHR/Fetch hooking and window global scanning
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
popup.js # Popup logic
results.js # Dashboard logic with filtering and export
css/
@@ -132,10 +136,22 @@ 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
@@ -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
View File
@@ -39,23 +39,39 @@
"__ENV__", "__CONFIG__", "ENV", "CONFIG",
];
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 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();
}
const origXhrOpen = XMLHttpRequest.prototype.open;
+2
View File
@@ -1,6 +1,8 @@
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);
+3 -1
View File
@@ -3,6 +3,8 @@ 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 || [];
@@ -214,7 +216,7 @@ function exportJson() {
function csvSafe(value) {
let str = String(value || "");
if (/^[=+\-@\t\r]/.test(str)) str = "'" + str;
if (/^[=+\-@\t\r\n]/.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.0",
"version": "2.1.1",
"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.0",
"version": "2.1.1",
"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">v2.0</span>
<span class="version" id="versionLabel"></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">v2.0</span></h1>
<h1>KeyFinder <span class="version" id="versionLabel"></span></h1>
</div>
<div class="header-actions">
<div class="filter-group">