Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- run: npm ci

- name: Lint
run: npx eslint .

- name: Format check
run: npx prettier --check '**/*.{js,json,md,html}'

- name: Syntax check
run: |
node --check background.js
node --check content.js
node --check popup.js
node --check bridge.js
node --check known-extensions.js

- name: Tests
run: npm test
2 changes: 2 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
npx lint-staged
npm test
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"semi": true
}
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Block LinkedIn's hidden extension scanning, device fingerprinting, and tracker p
## What LinkedIn Does

Every time you visit linkedin.com, hidden JavaScript:

- Probes **6,236 browser extensions** by ID to detect what you have installed
- Collects **48+ device data points** (CPU cores, memory, screen, battery, timezone)
- Injects a **zero-pixel invisible iframe** from HUMAN Security (li.protechts.net)
Expand All @@ -17,42 +18,49 @@ Source: [BrowserGate investigation](https://www.bleepingcomputer.com/news/securi
## What LinkedIn Shield Does

### Standalone Mode (no LLM needed)

- Blocks extension probing (intercepts `chrome-extension://` URL checks)
- Blocks tracker endpoints (`/li/track`, `/sensorCollect`, `protechts.net`)
- Randomizes device fingerprint data (CPU cores, memory, battery)
- Removes hidden tracking iframes
- Shows real-time badge count of blocked probes

### AI Analysis Mode (optional)

- Click "Analyze with AI" to get a plain-English explanation of what was blocked
- Supports Claude (Anthropic), OpenAI, QMax, or any OpenAI-compatible provider
- BYOLLM — bring your own API key, stored locally in browser storage

## Install

### From Source (Developer)

1. Clone this repo
2. Open `chrome://extensions/` → Enable Developer Mode
3. Click "Load unpacked" → Select the `linkedin-shield` folder
4. Visit linkedin.com and check the shield badge

### Chrome Web Store

Coming soon.

## How It Works

**Layer 1: Declarative Net Request Rules** (`rules.json`)

- Blocks tracking endpoints at the network level before JavaScript runs
- Blocks `protechts.net` iframe, `sensorCollect`, `/li/track`, `spectroscopy`

**Layer 2: Content Script** (`content.js`)

- Intercepts `fetch()` and `XMLHttpRequest` to block `chrome-extension://` probes
- Overrides `performance.getEntriesByName()` to prevent timing-based detection
- Randomizes `navigator.hardwareConcurrency` and `navigator.deviceMemory`
- Blocks `navigator.getBattery()` API
- MutationObserver removes hidden iframes as they're injected

**Layer 3: AI Analysis** (optional)

- Background service worker sends blocked stats to your chosen LLM
- Returns a plain-English privacy risk assessment
- No data leaves your machine unless you click "Analyze"
Expand All @@ -74,6 +82,7 @@ Coming soon.
## Contributing

PRs welcome. Key areas:

- Add more LinkedIn tracking endpoints as they're discovered
- Firefox Manifest V2 port
- Better fingerprint randomization
Expand Down
169 changes: 38 additions & 131 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@
*/

// ── Early injection: inject content.js into MAIN world before page scripts ──
chrome.webNavigation?.onCommitted?.addListener((details) => {
if (details.frameId !== 0) return; // top frame only
if (!details.url.includes('linkedin.com')) return;
chrome.scripting.executeScript({
target: { tabId: details.tabId },
files: ['content.js'],
world: 'MAIN',
injectImmediately: true,
}).catch(() => {}); // Ignore errors on restricted pages
}, { url: [{ hostContains: 'linkedin.com' }] });
chrome.webNavigation?.onCommitted?.addListener(
(details) => {
if (details.frameId !== 0) return; // top frame only
if (!details.url.includes('linkedin.com')) return;
chrome.scripting
.executeScript({
target: { tabId: details.tabId },
files: ['content.js'],
world: 'MAIN',
injectImmediately: true,
})
.catch(() => {}); // Ignore errors on restricted pages
},
{ url: [{ hostContains: 'linkedin.com' }] },
);

// Per-tab stats
const tabStats = {};
Expand All @@ -35,7 +40,10 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// Update badge and tooltip
const total = msg.total || 0;
chrome.action.setBadgeText({ text: total > 0 ? String(total) : '', tabId });
chrome.action.setBadgeBackgroundColor({ color: total > 50 ? '#ef4444' : total > 10 ? '#f59e0b' : '#22c55e', tabId });
chrome.action.setBadgeBackgroundColor({
color: total > 50 ? '#ef4444' : total > 10 ? '#f59e0b' : '#22c55e',
tabId,
});
chrome.action.setTitle({
title: `LinkedIn Shield — ${total} blocked\n${msg.probes || 0} extension probes\n${msg.fingerprints || 0} fingerprint APIs spoofed\n${msg.trackers || 0} trackers blocked`,
tabId,
Expand All @@ -52,134 +60,33 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
sendResponse(result || { probes: 0, fingerprints: 0, trackers: 0, total: 0 });
return true;
}

if (msg.type === 'ai_analyze') {
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout after 30s')), 30000));
Promise.race([handleAIAnalysis(msg.stats), timeout]).then(result => {
sendResponse(result);
}).catch(err => {
sendResponse({ error: `Analysis failed: ${err.message}` });
});
return true;
}
});

// Track blocked requests via declarativeNetRequest (only in dev mode with Feedback permission)
if (chrome.declarativeNetRequest?.onRuleMatchedDebug) {
chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((info) => {
const tabId = info.request.tabId;
if (tabId > 0) {
if (!tabStats[tabId]) {
tabStats[tabId] = { probes: 0, fingerprints: 0, trackers: 0, total: 0, timestamp: Date.now() };
try {
chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((info) => {
const tabId = info.request.tabId;
if (tabId > 0) {
if (!tabStats[tabId]) {
tabStats[tabId] = { probes: 0, fingerprints: 0, trackers: 0, total: 0, timestamp: Date.now() };
}
tabStats[tabId].trackers++;
tabStats[tabId].total++;
const total = tabStats[tabId].total;
chrome.action.setBadgeText({ text: total > 0 ? String(total) : '', tabId });
chrome.action.setBadgeBackgroundColor({
color: total > 50 ? '#ef4444' : total > 10 ? '#f59e0b' : '#22c55e',
tabId,
});
}
tabStats[tabId].trackers++;
tabStats[tabId].total++;
const total = tabStats[tabId].total;
chrome.action.setBadgeText({ text: total > 0 ? String(total) : '', tabId });
chrome.action.setBadgeBackgroundColor({ color: total > 50 ? '#ef4444' : total > 10 ? '#f59e0b' : '#22c55e', tabId });
}
});
} catch (e) { /* Feedback permission not granted — debug listener unavailable */ }
});
} catch (_e) {
/* Feedback permission not granted — debug listener unavailable */
}
}

// Clean up tab stats when tab closes
chrome.tabs.onRemoved.addListener((tabId) => {
delete tabStats[tabId];
});

// ── AI Analysis Mode (optional — requires API key) ───────────────────

async function handleAIAnalysis(stats) {
const result = await chrome.storage.local.get(['ai_api_key', 'ai_provider', 'ai_api_base', 'ai_model']);
const apiKey = result.ai_api_key;
const provider = result.ai_provider || 'anthropic';

if (!apiKey) {
return { error: 'No API key configured. Go to Settings to add one.' };
}

const ctx = stats.context || {};
const extSample = (ctx.extensionIds || []).slice(0, 10).join(', ') || 'none captured';
const matchedNames = (ctx.matchedExtensions || []).slice(0, 10).map(e => `${e.name} (${e.category})`).join(', ') || 'lookup not available';
const blockedSample = (ctx.blockedUrls || []).join('\n ') || 'none';
const fpApis = (ctx.fingerprintApis || []).join(', ') || 'none';

const prompt = `You are a privacy security analyst. A user visited LinkedIn and the following surveillance was detected and blocked:

EXTENSION PROBING: ${stats.probes || 0} unique chrome-extension:// URLs probed (LinkedIn checking which extensions are installed)
Known extensions LinkedIn checked for: ${matchedNames}
Sample raw extension IDs: ${extSample}

DEVICE FINGERPRINTING: ${stats.fingerprints || 0} APIs intercepted and spoofed: ${fpApis}

SURVEILLANCE ENDPOINTS BLOCKED: ${stats.trackers || 0}
${blockedSample}

HIDDEN IFRAMES: ${ctx.iframesRemoved || 0} invisible tracking iframes from HUMAN Security (protechts.net) removed

Explain in 4-5 sentences:
1. What specific data LinkedIn was trying to collect from this user
2. What the extension probing reveals (job hunting tools, ad blockers, accessibility tools, etc.)
3. How device fingerprinting creates a unique ID that follows you across sites
4. The real-world privacy risk and what LinkedIn could do with this data
Be specific to the numbers above. Direct and factual, no fluff.`;

let apiBase, model, headers;

if (provider === 'anthropic') {
apiBase = 'https://api.anthropic.com/v1';
model = 'claude-haiku-4-5-20251001';
headers = {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true',
};

const resp = await fetch(`${apiBase}/messages`, {
method: 'POST',
headers,
body: JSON.stringify({
model,
max_tokens: 200,
messages: [{ role: 'user', content: prompt }],
}),
});

if (!resp.ok) {
const errText = await resp.text();
return { error: `Claude API error (${resp.status}): ${errText.slice(0, 150)}` };
}

const data = await resp.json();
return { analysis: data.content?.[0]?.text || 'No response.' };
}

// OpenAI-compatible format (works with OpenAI, QMax/Qwen, DeepSeek, etc.)
apiBase = (result.ai_api_base || 'https://api.openai.com/v1').replace(/\/$/, '');
model = result.ai_model || 'gpt-4o-mini';

const resp = await fetch(`${apiBase}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: model,
max_tokens: 200,
messages: [
{ role: 'system', content: 'You are a privacy security analyst. Be direct and factual.' },
{ role: 'user', content: prompt },
],
}),
});

if (!resp.ok) {
const errText = await resp.text();
return { error: `API error (${resp.status}): ${errText.slice(0, 150)}` };
}

const data = await resp.json();
return { analysis: data.choices?.[0]?.message?.content || 'No response.' };
}
32 changes: 20 additions & 12 deletions content.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
(function () {
'use strict';

if (window.__linkedinShieldActive) return;
window.__linkedinShieldActive = true;
const SHIELD_KEY = Symbol.for('__linkedinShieldActive');
if (window[SHIELD_KEY]) return;
Object.defineProperty(window, SHIELD_KEY, { value: true, writable: false, configurable: false });
if (window !== window.top) return;

let probeCount = 0;
Expand All @@ -37,7 +38,7 @@
if (blockedUrls.length < 10) blockedUrls.push(url.substring(0, 80));
}
return Reflect.apply(target, thisArg, args);
}
},
});
Object.defineProperty(window, 'fetch', { value: fetchProxy, writable: true, configurable: true });

Expand All @@ -49,16 +50,24 @@
}
});
po.observe({ type: 'resource', buffered: true });
} catch (e) {}
} catch (_e) {}

// ── 3. Fingerprint spoofing ─────────────────────────────────────────
try { Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 }); } catch (e) {}
try { if ('deviceMemory' in navigator) Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 }); } catch (e) {}
try {
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 });
} catch (_e) {}
try {
if ('deviceMemory' in navigator) Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
} catch (_e) {}
if ('getBattery' in navigator) {
navigator.getBattery = () => Promise.resolve({
charging: true, chargingTime: 0, dischargingTime: Infinity,
level: 1.0, addEventListener: () => {}
});
navigator.getBattery = () =>
Promise.resolve({
charging: true,
chargingTime: 0,
dischargingTime: Infinity,
level: 1.0,
addEventListener: () => {},
});
}

// ── 4. Remove surveillance iframes ──────────────────────────────────
Expand Down Expand Up @@ -93,11 +102,10 @@
},
};
document.documentElement.setAttribute('data-linkedin-shield', JSON.stringify(data));
window.postMessage({ type: 'linkedin_shield_stats', ...data }, '*');
window.postMessage({ type: 'linkedin_shield_stats', ...data }, window.location.origin);
}

setInterval(writeStats, 3000);
setTimeout(writeStats, 2000);
setTimeout(writeStats, 5000);

})();
Loading
Loading