mirror of
https://github.com/momenbasel/keyFinder.git
synced 2026-06-07 08:33:54 +02:00
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.
This commit is contained in:
@@ -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"]
|
||||
@@ -0,0 +1,56 @@
|
||||
# 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
@@ -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",
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user