Skip to content

Commit 604e326

Browse files
security(audit): build structured sensitive domain matchers and CSP permission catalog
1 parent 6220275 commit 604e326

4 files changed

Lines changed: 291 additions & 57 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Security Hardening & Permissions Audit
2+
3+
This document details the Manifest V3 privilege structure, strict Content Security Policies (CSP), and runtime isolation boundaries built into the **Web-Swap Tracker** browser extension.
4+
5+
---
6+
7+
## 1. Permissions Inventory (Least Privilege Compliance)
8+
9+
To guarantee the user's data privacy and security, this extension implements a strict deny-by-default permission profile. No wildcards or open scopes are requested.
10+
11+
| Permission | Context of Usage | Rationale & Privacy Guarantee | Alternate Designs Evaluated |
12+
| :--- | :--- | :--- | :--- |
13+
| **`storage`** | Background & Popup & Content | Required to persist the user's local custom productivity rules and Blob UI coordinates. **Guarantee**: Uses local browser storage ONLY; zero synchronization or remote telemetry. | Evaluated SQLite/FileSystem API: rejected due to unnecessary sandbox escape vectors. |
14+
| **`tabs`** | Background Service Worker | Used strictly in tracking tab change transitions to capture precise active sessions (e.g. `chrome.tabs.onActivated`). **Guarantee**: Never tracks page HTML content, input values, or cookie hashes. | Evaluated activeTab: rejected because background tracking must function continuously. |
15+
| **`activeTab`** | Dynamic Actions | Granted dynamically on user action to interact with active pages safely. | None. Standard Chrome mechanism for contextual access. |
16+
17+
---
18+
19+
## 2. Strict Content Security Policy (CSP)
20+
21+
The extension employs a rigid Content Security Policy strictly aligned with the highest Chrome Web Store security standards.
22+
23+
```json
24+
"content_security_policy": {
25+
"extension_pages": "default-src 'self'; script-src 'self'; object-src 'none';"
26+
}
27+
```
28+
29+
### Security Hardening Measures
30+
1. **No External Scripts (`script-src 'self'`)**:
31+
- Strictly forbids the execution of scripts loaded from remote CDNs, third-party trackers, or servers.
32+
- Prevents Man-in-the-Middle (MitM) script injection vectors.
33+
2. **No Dynamic Execution (`unsafe-eval` banned)**:
34+
- Evaluators like `eval()`, `setTimeout(string)`, and `new Function()` are completely blocked.
35+
- Prevents DOM-based cross-site scripting (XSS) via string-to-code evaluation.
36+
3. **No Unsafe Inline Styles (`style-src 'self'`)**:
37+
- Blocks dynamic inline style injection.
38+
- Prevents CSS injection attacks from spoofing the premium dashboard interface.
39+
40+
---
41+
42+
## 3. Strict Context Isolation Boundaries
43+
44+
The extension interacts across three distinct runtime zones:
45+
```mermaid
46+
graph TD
47+
A[Webpage Host Context] -- Isolated DOM / Shadow DOM --> B(Content Script UI)
48+
B -- Runtime Messaging satisfies RuntimeMessage --> C{Deny-by-Default Gateway}
49+
C -- Allowed Capabilities --> D[Privileged Background Worker]
50+
C -- Block / Log --> E[Local Security Ring-Buffer]
51+
```
52+
53+
### Content Script UI Isolation (`content.tsx`)
54+
- **Shadow DOM Isolation**: The Floating Blob UI renders inside an isolated Shadow DOM container. This shields the extension's controls and style rules from context leaks or manipulation by malicious third-party script assets on the active webpage.
55+
- **Protocol Verification**: Content scripts are strictly excluded from injecting inside sensitive domains (like bank sign-ins, payment gates, or identity portals) using regex match lists and configuration protocol checks.
56+
57+
### Background Message Gateway Defense (`background.ts`)
58+
- **Origin derivation (`deriveSurface`)**: Validates the sender parameters on every message (`sender.id === chrome.runtime.id`). Context is classified using strict URL analysis (e.g. distinguishing `content` tab origins from privileged `popup.html` and `dashboard.html` paths).
59+
- **Capability Mapping (`MESSAGE_CAPABILITIES`)**: Map that associates message action keys with allowed context surfaces. For example, webpage content scripts are restricted from performing mutations such as `SAVE_PRODUCTIVITY_RULES` or `TOGGLE_TRACKING` – attempts automatically trigger high-fidelity local security incident alerts.
60+
- **Telemetry-Free Diagnostics (`RingBuffer`)**: All malformed structures, coordinates overruns, or spoofing vectors are recorded inside an on-device 50-entry chronologically organized Ring Buffer and atomic counter maps. Zero data is transmitted off the device.

src/security/security-fixtures.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* security-fixtures.ts
3+
*
4+
* Security test fixtures for manual validation and automated regression testing.
5+
* Contains malformed payloads, spoofed sender context objects, and coordinate overflows
6+
* to verify the robustness of validators and message dispatch firewalls.
7+
*/
8+
9+
import type { RuntimeMessage } from "../types/tracking";
10+
import type { BlobUIState } from "./validators";
11+
12+
/**
13+
* Spoofed sender origin configurations mimicking various surfaces.
14+
*/
15+
export const SENDER_FIXTURES = Object.freeze({
16+
/** authentic content script context */
17+
validContentScript: Object.freeze({
18+
id: "extension-id-placeholder", // Will match runtime.id in tests
19+
url: "https://github.com/login",
20+
tab: { id: 101, index: 0, pinned: false, windowId: 1, active: true }
21+
} as unknown as chrome.runtime.MessageSender),
22+
23+
/** authentic privileged options tab context */
24+
validDashboardTab: Object.freeze({
25+
id: "extension-id-placeholder",
26+
url: "chrome-extension://extension-id-placeholder/tabs/dashboard.html",
27+
tab: undefined
28+
} as unknown as chrome.runtime.MessageSender),
29+
30+
/** authentic privileged popup context */
31+
validPopupTab: Object.freeze({
32+
id: "extension-id-placeholder",
33+
url: "chrome-extension://extension-id-placeholder/popup.html",
34+
tab: undefined
35+
} as unknown as chrome.runtime.MessageSender),
36+
37+
/** malicious external website origin trying to send extension messages */
38+
maliciousExternalPage: Object.freeze({
39+
id: undefined,
40+
url: "https://malicious-phishing-site.com",
41+
tab: { id: 202, index: 1, pinned: false, windowId: 2, active: true }
42+
} as unknown as chrome.runtime.MessageSender),
43+
44+
/** rouge cross-extension origin attempting connection spoofing */
45+
rogueCrossExtension: Object.freeze({
46+
id: "some-other-hostile-extension-id",
47+
url: "chrome-extension://some-other-hostile-extension-id/popup.html",
48+
tab: undefined
49+
} as unknown as chrome.runtime.MessageSender)
50+
});
51+
52+
/**
53+
* Corrupted local storage coordinates for Blob UI test validation.
54+
*/
55+
export const STORAGE_COORDINATE_FIXTURES = Object.freeze({
56+
/** standard clean positions */
57+
pristine: Object.freeze({
58+
anchorCorner: "bottom-right",
59+
offsetX: 24,
60+
offsetY: 24,
61+
isCollapsed: true
62+
}),
63+
64+
/** absurd coordinate values designed to break layouts */
65+
layoutOverflowGiant: Object.freeze({
66+
anchorCorner: "top-left",
67+
offsetX: 999999,
68+
offsetY: 1234567,
69+
isCollapsed: false
70+
}),
71+
72+
/** invalid string values causing parsing errors */
73+
corruptStringValues: Object.freeze({
74+
anchorCorner: "invalid-corner-name",
75+
offsetX: "one-hundred-pixels",
76+
offsetY: NaN,
77+
isCollapsed: "not-a-boolean"
78+
} as unknown as BlobUIState),
79+
80+
/** null or undefined fields */
81+
nullifiedObject: null
82+
});
83+
84+
/**
85+
* Payload fixtures for the runtime messaging protocol.
86+
*/
87+
export const RUNTIME_MESSAGE_FIXTURES = Object.freeze({
88+
/** authentic payload containing rule updates */
89+
validSaveRules: Object.freeze({
90+
type: "SAVE_PRODUCTIVITY_RULES",
91+
version: 1,
92+
rules: [
93+
{
94+
domain: "distracting.com",
95+
category: "distracting",
96+
priority: 10,
97+
createdAt: Date.now()
98+
}
99+
]
100+
} as unknown as RuntimeMessage),
101+
102+
/** payload missing vital version boundaries */
103+
missingVersionPayload: Object.freeze({
104+
type: "GET_TODAY_STATS"
105+
// version is missing
106+
} as unknown as RuntimeMessage),
107+
108+
/** payload referencing an obsolete or unsupported protocol version */
109+
unsupportedVersionPayload: Object.freeze({
110+
type: "GET_TODAY_STATS",
111+
version: 99 // Strict firewall will reject this
112+
} as unknown as RuntimeMessage),
113+
114+
/** payload holding an unknown event action type */
115+
unknownMessageTypePayload: Object.freeze({
116+
type: "FORMAT_HARD_DRIVE", // Unregistered event action key
117+
version: 1
118+
} as unknown as RuntimeMessage),
119+
120+
/** malformed fields on valid types */
121+
malformedFieldPayload: Object.freeze({
122+
type: "TOGGLE_TRACKING",
123+
version: 1,
124+
paused: "true-as-a-string" // should be strict boolean
125+
} as unknown as RuntimeMessage),
126+
127+
/** empty payload structures */
128+
emptyPayload: Object.freeze({})
129+
});

src/security/sensitive-sites.ts

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,81 @@
1+
import { safeParseUrl } from "../utils/url";
2+
13
/**
24
* sensitive-sites.ts
35
*
46
* Centralized checker to flag high-sensitivity URLs that should avoid
57
* any content script UI injection (e.g., banking domains, login portals,
6-
* credential management systems, localhost auth gateways).
7-
*
8-
* RATIONALE:
9-
* Injecting UI elements on highly sensitive pages (such as banking sites or
10-
* authentication logins) can trigger security filters, looks suspicious to
11-
* security-conscious users, and increases risk of store rejection.
8+
* credential management systems, system URLs).
129
*/
1310

14-
const SENSITIVE_PATTERNS = [
11+
export interface SensitiveSitePattern {
12+
readonly category: "auth" | "financial" | "system" | "manager";
13+
readonly pattern: RegExp;
14+
}
15+
16+
/**
17+
* Structured sensitive site patterns catalog.
18+
* Deeply frozen to prevent runtime mutation or injection.
19+
*/
20+
export const SENSITIVE_PATTERNS: readonly SensitiveSitePattern[] = Object.freeze([
1521
// Authentication & Core Providers
16-
/accounts\.google\.com/i,
17-
/login\.microsoftonline\.com/i,
18-
/appleid\.apple\.com/i,
19-
/github\.com\/login/i,
20-
/auth\./i,
21-
/oauth/i,
22-
/signin/i,
23-
/signup/i,
24-
25-
// Financial Services & Payment Gateways
26-
/paypal\.com/i,
27-
/stripe\.com/i,
28-
/chase\.com/i,
29-
/bankofamerica\.com/i,
30-
/wellsfargo\.com/i,
31-
/citibank\.com/i,
32-
/fidelity\.com/i,
22+
{ category: "auth", pattern: /accounts\.google\.com/i },
23+
{ category: "auth", pattern: /login\.microsoftonline\.com/i },
24+
{ category: "auth", pattern: /appleid\.apple\.com/i },
25+
{ category: "auth", pattern: /github\.com\/login/i },
26+
{ category: "auth", pattern: /auth\./i },
27+
{ category: "auth", pattern: /oauth/i },
28+
{ category: "auth", pattern: /signin/i },
29+
{ category: "auth", pattern: /signup/i },
30+
31+
// Password Managers
32+
{ category: "manager", pattern: /bitwarden\.com/i },
33+
{ category: "manager", pattern: /1password\.com/i },
34+
{ category: "manager", pattern: /lastpass\.com/i },
35+
{ category: "manager", pattern: /dashlane\.com/i },
36+
37+
// Financial Services & Banking
38+
{ category: "financial", pattern: /paypal\.com/i },
39+
{ category: "financial", pattern: /stripe\.com/i },
40+
{ category: "financial", pattern: /chase\.com/i },
41+
{ category: "financial", pattern: /bankofamerica\.com/i },
42+
{ category: "financial", pattern: /wellsfargo\.com/i },
43+
{ category: "financial", pattern: /citibank\.com/i },
44+
{ category: "financial", pattern: /fidelity\.com/i },
45+
{ category: "financial", pattern: /schwab\.com/i },
46+
{ category: "financial", pattern: /capitalone\.com/i },
47+
{ category: "financial", pattern: /hsbc\.com/i },
48+
{ category: "financial", pattern: /barclays\.co\.uk/i },
49+
50+
// Web3 & Crypto Wallets
51+
{ category: "financial", pattern: /metamask\.io/i },
52+
{ category: "financial", pattern: /phantom\.app/i },
53+
{ category: "financial", pattern: /coinbase\.com/i },
54+
{ category: "financial", pattern: /binance\.com/i },
3355

3456
// System & Internal Sites
35-
/^chrome:\/\//i,
36-
/^chrome-extension:\/\//i,
37-
/^about:/i,
38-
/^file:\/\//i
39-
];
57+
{ category: "system", pattern: /^chrome:\/\//i },
58+
{ category: "system", pattern: /^chrome-extension:\/\//i },
59+
{ category: "system", pattern: /^about:/i },
60+
{ category: "system", pattern: /^file:\/\//i }
61+
]);
4062

4163
/**
4264
* Checks if a given URL is a sensitive domain/page.
4365
* Returns true if injection should be blocked.
66+
* Leverages secure safeParseUrl for defense-in-depth scheme checks.
4467
*/
4568
export function isSensitiveSite(url: string | undefined): boolean {
46-
if (!url) return true;
47-
48-
try {
49-
// Basic format check
50-
if (!url.startsWith("http://") && !url.startsWith("https://")) {
51-
return true; // Exclude system, file, and blank protocols
52-
}
53-
54-
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(url));
55-
} catch (error) {
56-
// If URL parsing or pattern matching fails, fail-safe and treat as sensitive
57-
return true;
69+
if (!url) {
70+
return true; // Fail-safe
5871
}
72+
73+
// centralize parsing through our safety validator (which strips unsafe schemes)
74+
const parsed = safeParseUrl(url);
75+
if (!parsed) {
76+
return true; // If parse fails or scheme is dangerous, fail-secure and treat as sensitive
77+
}
78+
79+
const fullUrl = parsed.toString();
80+
return SENSITIVE_PATTERNS.some((entry) => entry.pattern.test(fullUrl));
5981
}

src/utils/url.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,63 @@
11
import { logger } from "./logger";
22

33
/**
4-
* Extracts and preserves the full hostname from a URL.
5-
* Only allows http: and https: protocols.
6-
* Strips path, query, hash, etc.
7-
* E.g., https://docs.github.com/path?query -> docs.github.com
4+
* Normalizes a domain name by removing leading and trailing dots and lowercase conversion.
85
*/
96
export function normalizeDomain(domain: string): string {
107
return domain.trim().toLowerCase().replace(/^\.+|\.+$/g, "");
118
}
129

1310
/**
14-
* Extracts and preserves the full hostname from a URL.
15-
* Only allows http: and https: protocols.
16-
* Strips path, query, hash, etc.
17-
* E.g., https://docs.github.com/path?query -> docs.github.com
11+
* Standard list of explicitly denied dangerous URL schemes.
12+
* Deeply frozen to prevent mutation.
1813
*/
19-
export function extractHostname(urlStr: string | undefined): string | null {
14+
export const DENIED_URL_SCHEMES = Object.freeze([
15+
"javascript:",
16+
"data:",
17+
"blob:",
18+
"filesystem:"
19+
]);
20+
21+
/**
22+
* Centralized, secure URL parser that wraps V8 instantiation.
23+
* Rejects malformed strings, dangerous schemes, and non-web protocols safely.
24+
*/
25+
export function safeParseUrl(urlStr: string | undefined): URL | null {
2026
if (!urlStr) {
2127
return null;
2228
}
2329

2430
try {
2531
const parsedUrl = new URL(urlStr);
26-
32+
const protocol = parsedUrl.protocol.toLowerCase();
33+
34+
// Explicitly reject dangerous schemes
35+
if (DENIED_URL_SCHEMES.includes(protocol)) {
36+
logger.debug(`[URL Safety] Explicitly denied URL scheme rejected: ${protocol}`);
37+
return null;
38+
}
39+
2740
// Strict whitelist: only HTTP and HTTPS
28-
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
29-
logger.debug(`[URL Util] Ignored protocol: ${parsedUrl.protocol}`);
41+
if (protocol !== "http:" && protocol !== "https:") {
42+
logger.debug(`[URL Safety] Ignored non-web protocol: ${protocol}`);
3043
return null;
3144
}
3245

33-
// Preserve the full hostname (e.g. docs.github.com)
34-
return normalizeDomain(parsedUrl.hostname);
35-
} catch (error) {
36-
logger.debug(`[URL Util] Failed to parse URL: ${urlStr}`);
46+
return parsedUrl;
47+
} catch (_error) {
48+
logger.debug(`[URL Safety] Failed to securely parse URL: ${urlStr}`);
3749
return null;
3850
}
3951
}
4052

53+
/**
54+
* Extracts and normalizes the hostname from a URL.
55+
* Leverages safeParseUrl for strict scheme and security parsing.
56+
*/
57+
export function extractHostname(urlStr: string | undefined): string | null {
58+
const parsed = safeParseUrl(urlStr);
59+
if (!parsed) {
60+
return null;
61+
}
62+
return normalizeDomain(parsed.hostname);
63+
}

0 commit comments

Comments
 (0)