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
9 changed files with 21 additions and 164 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
@@ -60,7 +60,7 @@ jobs:
- name: Attach to GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v3
uses: softprops/action-gh-release@v2
with:
files: |
dist/keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip
-56
View File
@@ -1,56 +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).
## [Unreleased]
### 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
- 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
-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
@@ -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">