Is google meet extension safe?
Google Meet is high risk. On every page visit, any site, the extension sends the URL, referrer, and a persistent ID to stream.meetextension.com, AES-GCM encrypted with a hardcoded key. DA captured 6 POSTs to /process decrypting to google.com and other sites, HTTP.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Browsing History Sent to stream.meetextension.com on Every Navigation
On every page visit, any site, the extension sends the URL, referrer, and a persistent ID to stream.meetextension.com, AES-GCM encrypted with a hardcoded key.
DA captured 6 POSTs to /process decrypting to google.com and other sites, HTTP.
You navigate to any web page, any site, anywhere, in any tab.
The extension records the URL you visited, the URL you came from, and your persistent user ID, then encrypts and POSTs them to stream.meetextension.com.
Fires on all sites, not just meet.google.com. Telemetry consent is stored as a flag that can be set by the extension's popup, but the listener is active regardless of whether a consent prompt was presented during initial install.
| Content-Type | application/json;charset=utf-8 |
| Authorization | Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIyNTJiYjZjZi0xOGE4LTQ0OWUtYmFkNy1mZjUzZWQyYTUxNGYiLCJpYXQiOjE3NDQ3MDAwMDB9.example |
{"eventType":1,"request":{"enRequest":"\"AAECBAUGB+vXzpqLkYz8Sl3mVHKxOeT5DqFbgBCAiJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycq/y83Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+""}}The telemetry payload is AES-GCM encrypted before transmission. The encryption key is hardcoded in the extension source as '0ipnBjZ7oFir6SVP'. Because the key is public (any user can read it from the extension files), anyone who captures the network traffic can decrypt the payloads and read the visited URLs.
[
{
"fileDate": "2026-04-15T09:48:07.123Z",
"deviceTimestamp": 1744710487123,
"userId": "252bb6cf-18a8-449e-bad7-ff53ed2a514f",
"referrerUrl": "https://www.google.com/",
"targetUrl": "https://www.amazon.com/",
"requestType": "GET"
}
]| Field | Value | Why it matters | |
|---|---|---|---|
The URL you are visiting | https://www.amazon.com/dp/B0CXYZ12345 | The exact page you navigated to, including any query parameters. | |
The URL you came from | https://www.google.com/search?q=amazon+deals | The previous page you were on in that tab, builds a chain of your browsing activity. | |
Your persistent user ID | 252bb6cf-18a8-449e-bad7-ff53ed2a514f | A UUID that stays the same across sessions, letting the server link all your visits together over time. | |
Timestamp | 2026-04-15T09:48:07.123Z | When the visit happened, in both ISO date format and Unix milliseconds. | |
Request type | GET | Always 'GET', a fixed label the server uses to classify the event type. |
The navigation listener and encryption logic from the extension source
// Three hardcoded values passed at instantiation time — all public in the extension source. const telemetry = new TelemetryClass( apiKey = "<redacted>", // POST to /auth to get bearer tokens aesKey = "0ipnBjZ7oFir6SVP", // 128-bit AES-GCM encryption key (UTF-8) endpoint = "http://stream.meetextension.com" // cleartext HTTP — no TLS );
// No domain allowlist. Fires for any URL starting with 'http'.
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete') {
const currentUrl = tab.url;
const tabState = getTabState(tabId);
const previousUrl = tabState.url;
// Reset referrer on cross-origin navigation
if (tabState.hasTransition) previousUrl = null;
// Report if url is HTTP and has changed
if (currentUrl.startsWith('http') && currentUrl !== previousUrl) {
telemetry.reportAction(currentUrl, previousUrl);
}
tabStates[tabId] = { url: currentUrl };
}
});async function encryptData(plaintext) {
const keyBytes = new TextEncoder().encode(aesKey); // '0ipnBjZ7oFir6SVP'
const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', true, ['encrypt']);
const iv = crypto.getRandomValues(new Uint8Array(16));
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(plaintext));
// Prepend IV to ciphertext, base64-encode the combined buffer
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
combined.set(iv);
combined.set(new Uint8Array(ciphertext), iv.length);
return btoa(String.fromCharCode(...combined));
}- stream.meetextension.com
Telemetry endpoint receiving one POST per page navigation. Payload contains visited URL, referrer URL, persistent user UUID, and timestamp. Operated by the extension developer.
- api.meetextension.com
Authentication API that issues bearer tokens used to authorize telemetry submissions. Also receives sign-in credentials over cleartext HTTP (see CWE-319 claim).
Decrypts any captured enRequest payload from http://stream.meetextension.com/process using the hardcoded AES-GCM key found in the extension source. Run it with a captured base64 payload to reveal the plaintext browsing data.
/**
* meet-ext-decrypt.js
* Decrypts enRequest payloads from Google Meet Extension (dghkhbmpagbapkadlehcicngkldfieln)
* using the hardcoded AES-GCM key from background.bundle.js:2810.
*
* Usage:
* node meet-ext-decrypt.js
* node meet-ext-decrypt.js '<base64_enRequest_value>'
*
* Requires: Node.js 18+ (built-in webcrypto)
*/
const crypto = require('crypto');
// Hardcoded key extracted from background.bundle.js line 2810:
// new function(e, t, r) { ... }("KcDvSvXBxOvVCk7z", "0ipnBjZ7oFir6SVP", "http://stream.meetextension.com")
const AES_KEY = "0ipnBjZ7oFir6SVP"; // 16 chars = 128-bit AES-GCM key
async function decrypt(base64Payload) {
// Strip surrounding JSON quotes if present
const clean = base64Payload.replace(/^"|"$/g, '');
const raw = Buffer.from(clean, 'base64');
// First 16 bytes are the IV, rest is ciphertext
const iv = raw.slice(0, 16);
const ciphertext = raw.slice(16);
const keyBytes = Buffer.from(AES_KEY, 'utf8');
const key = await crypto.webcrypto.subtle.importKey(
'raw', keyBytes, 'AES-GCM', true, ['decrypt']
);
const plaintext = await crypto.webcrypto.subtle.decrypt(
{ name: 'AES-GCM', iv }, key, ciphertext
);
return new TextDecoder().decode(plaintext);
}
async function main() {
if (process.argv[2]) {
// Decrypt a specific captured payload
console.log('Decrypting captured payload...');
try {
const result = await decrypt(process.argv[2]);
console.log('Decrypted:', result);
// Pretty-print if valid JSON
try {
const parsed = JSON.parse(result);
console.log('\nParsed:');
if (Array.isArray(parsed)) {
parsed.forEach((entry, i) => {
console.log(` Entry ${i + 1}:`);
console.log(` targetUrl: ${entry.targetUrl}`);
console.log(` referrerUrl: ${entry.referrerUrl}`);
console.log(` userId: ${entry.userId}`);
console.log(` fileDate: ${entry.fileDate}`);
console.log(` deviceTimestamp: ${entry.deviceTimestamp}`);
});
}
} catch {}
} catch (e) {
console.error('Decryption failed:', e.message);
}
} else {
// Demo: show what a captured payload decrypts to
console.log('No payload provided. To decrypt a real captured request:');
console.log(' 1. Capture a POST to http://stream.meetextension.com/process in your browser DevTools.');
console.log(' 2. Copy the value of request.enRequest from the JSON body.');
console.log(' 3. Run: node meet-ext-decrypt.js \'<enRequest_value>\'');
console.log('');
console.log(`AES-GCM key (hardcoded in extension): ${AES_KEY}`);
console.log('Format: base64(IV[16 bytes] + AES-GCM-ciphertext)');
}
}
main().catch(console.error);
- 1Intercept a POST to stream.meetextension.com/process (DevTools or mitmproxy).
- 2Copy enRequest's inner base64 value, no quotes.
- 3Run: node meet-ext-decrypt.js '<value>'.
- 4Output shows plaintext targetUrl, referrerUrl, userId.