Is FDev Security safe?
FDev Security sends the hostname of every site you visit to its developer's own server, with no way to turn it off.
On every top-level page load, the extension checks the site's hostname against a locally cached allow list and, if it isn't there, sends that hostname to lenhatthanh.com/api/v1/domain-check.php. The server's response decides whether a warning screen is injected into the page. This check runs unconditionally at startup and, unlike the extension's other blocking features, has no corresponding setting in the options page to disable it.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
FDev Security reports every website you visit to the developer's own server
Code analysis shows the background service worker sends the hostname of every page you visit to the developer's own domain, lenhatthanh.com, for every new site.
There is no setting to turn off this per-navigation check-in.
You open any top-level webpage, from a search result to your work email.
The background service worker sends that page's hostname to the developer's own server as the page begins loading.
This runs for ordinary, unrelated sites like example.com or wikipedia.org, not just Facebook or Messenger.
| Field | Value | Why it matters | |
|---|---|---|---|
Visited hostname | en.wikipedia.org | The domain of the page you just opened, sent for every new top-level navigation. | |
Safety verdict | {"isSafety": true} | The server's flag telling the extension whether to show you an unsafe-site warning. |
The navigation listener and outbound POST, as shipped and deobfuscated
(() => {
let e = !0;
var t;
t = async t => {
var o;
e = !0;
const c = null !== (o = await (async e => {
const t = await chrome.storage.sync.get(e);
return t[e] ? t[e] : null
})("allowList")) && void 0 !== o ? o : [],
r = new URL(t.url).hostname;
if (!c.some((e => e.includes(r)))) try {
await (async t => {
var o;
const c = await fetch("https://lenhatthanh.com/api/v1/domain-check.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
domain: t
})
}),
r = await c.json();
e = null === (o = r.isSafety) || void 0 === o || o
})(r)
} catch (e) {
console.warn(e)
}
}, chrome.webRequest.onSendHeaders.addListener(t, {
urls: ["*://*/*"],
types: ["main_frame"]
}, ["requestHeaders"]), chrome.tabs.onUpdated.addListener(((t, o, c) => {
var r;
if ("complete" === o.status && !e && c) {
const e = null !== (r = c.url) && void 0 !== r ? r : "";
if (!e.startsWith("http://") && !e.startsWith("https://")) return;
const o = "./scripts/realtime-protection.content-script.js";
chrome.scripting.executeScript({
target: {
tabId: t
},
files: [o]
}, (() => {
if (chrome.runtime.lastError) {
const e = `Script injection failed: ${chrome.runtime.lastError.message}`;
console.warn(e)
} else((e, t) => {
chrome.tabs.sendMessage(e, t)
})(t, {
type: "dangerousSiteDetected",
url: e
})
}))
}
}))
})()
})();- lenhatthanh.com
Receives the hostname of every top-level page you visit and returns a safety verdict used to show or suppress the extension's own warning modal.
Loads the unpacked extension in Chromium and confirms a domain-check POST fires for ordinary, unrelated sites, not just Facebook or Messenger.
// verify_domain_check.js
// Loads the extension unpacked in real Chromium and confirms a POST to
// the domain-check endpoint fires for ordinary, unrelated sites, not just
// Facebook or Messenger.
const puppeteer = require('puppeteer');
const path = require('path');
(async () => {
const extensionPath = path.resolve(process.argv[2] || './fdev-security-unpacked');
const browser = await puppeteer.launch({
headless: false, // MV3 service workers need a headed context to load
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
],
});
const targets = ['https://example.com/', 'https://en.wikipedia.org/wiki/Special:Random'];
const seen = [];
for (const url of targets) {
const page = await browser.newPage();
page.on('request', (req) => {
if (req.url().startsWith('https://lenhatthanh.com/api/v1/domain-check.php')) {
seen.push({ visited: url, body: req.postData() });
}
});
await page.goto(url, { waitUntil: 'networkidle0' });
await new Promise((r) => setTimeout(r, 2000));
await page.close();
}
console.log(JSON.stringify(seen, null, 2));
console.log(
seen.length === targets.length
? 'CONFIRMED: a domain-check POST fired for every visited site.'
: 'NOT CONFIRMED: fewer domain-check POSTs than visited sites.'
);
await browser.close();
})();
- 1Unzip the extension's CRX to ./fdev-security-unpacked.
- 2npm i puppeteer.
- 3node verify_domain_check.js ./fdev-security-unpacked
Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.