mirror of
https://github.com/agresdominik/file-leak.git
synced 2026-04-21 18:05:48 +00:00
Unified function, simplified check scope, added further checks to avoid false positives
This commit is contained in:
@@ -14,9 +14,7 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"webNavigation",
|
"webNavigation",
|
||||||
"storage",
|
"storage",
|
||||||
"<all_urls>",
|
"<all_urls>"
|
||||||
"http://*/*",
|
|
||||||
"https://*/*"
|
|
||||||
],
|
],
|
||||||
|
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
|
|||||||
@@ -1,72 +1,23 @@
|
|||||||
// function to scan the url the tab is on for hidden files
|
|
||||||
|
|
||||||
async function onTabUpdate(tabId, changeInfo, tab) {
|
|
||||||
if (changeInfo.status !== "complete") return;
|
|
||||||
|
|
||||||
const url = new URL(tab.url);
|
|
||||||
const hostname = url.hostname;
|
|
||||||
|
|
||||||
const stored = await browser.storage.local.get("entries");
|
|
||||||
const existingEntries = stored.entries || [];
|
|
||||||
|
|
||||||
const alreadyDone = existingEntries.some(e => e.domainField === hostname);
|
|
||||||
if (alreadyDone) return;
|
|
||||||
|
|
||||||
async function tryFetch(pathname) {
|
|
||||||
const target = url.origin + pathname;
|
|
||||||
let response;
|
|
||||||
try {
|
|
||||||
response = await fetch(target, { redirect: "manual" });
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return response.status === 200 ? target : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = {
|
|
||||||
env: await tryFetch("/.env"),
|
|
||||||
git: await tryFetch("/.git"),
|
|
||||||
dsstore: await tryFetch("/.DS_Store"),
|
|
||||||
config: await tryFetch("/.config"),
|
|
||||||
svn: await tryFetch("/.svn"),
|
|
||||||
npm: await tryFetch("/.npm"),
|
|
||||||
hg: await tryFetch("/.hg"),
|
|
||||||
docker: await tryFetch("/.docker"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const newEntries = [...existingEntries];
|
|
||||||
|
|
||||||
for (const [key, foundPath] of Object.entries(results)) {
|
|
||||||
if (!foundPath) continue;
|
|
||||||
|
|
||||||
const entry = {
|
|
||||||
domainField: hostname,
|
|
||||||
pathField: foundPath,
|
|
||||||
type: key
|
|
||||||
};
|
|
||||||
|
|
||||||
newEntries.push(entry);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.storage.local.set({ entries: newEntries });
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable, Idsable automatic listener and the message listener for it
|
// Enable, Idsable automatic listener and the message listener for it
|
||||||
|
|
||||||
function enableListener() {
|
function enableListener() {
|
||||||
if (!browser.tabs.onUpdated.hasListener(onTabUpdate)) {
|
if (!browser.tabs.onUpdated.hasListener(tabUpdateListener)) {
|
||||||
browser.tabs.onUpdated.addListener(onTabUpdate);
|
browser.tabs.onUpdated.addListener(tabUpdateListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function disableListener() {
|
function disableListener() {
|
||||||
if (browser.tabs.onUpdated.hasListener(onTabUpdate)) {
|
if (browser.tabs.onUpdated.hasListener(tabUpdateListener)) {
|
||||||
browser.tabs.onUpdated.removeListener(onTabUpdate);
|
browser.tabs.onUpdated.removeListener(tabUpdateListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tabUpdateListener(tabId, changeInfo, tab) {
|
||||||
|
if (changeInfo.status === "complete" && tab.active) {
|
||||||
|
runSingleScan(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add and remove listeners based on message sent by frontend toggle
|
||||||
browser.runtime.onMessage.addListener((msg) => {
|
browser.runtime.onMessage.addListener((msg) => {
|
||||||
if (msg.type === "toggleListener") {
|
if (msg.type === "toggleListener") {
|
||||||
if (msg.enabled) enableListener();
|
if (msg.enabled) enableListener();
|
||||||
@@ -75,55 +26,115 @@ browser.runtime.onMessage.addListener((msg) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Singe run, can be merged with onTabUpdate function
|
// Singe run, can be merged with onTabUpdate function
|
||||||
|
async function runSingleScan(tab) {
|
||||||
|
|
||||||
|
console.log("Checking site...")
|
||||||
|
|
||||||
|
//const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
|
|
||||||
|
//if (!tabs.length) return;
|
||||||
|
//const tab = tabs[0];
|
||||||
|
|
||||||
async function runSingleScan() {
|
|
||||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
|
||||||
if (!tabs.length) return;
|
|
||||||
const tab = tabs[0];
|
|
||||||
const url = new URL(tab.url);
|
const url = new URL(tab.url);
|
||||||
const hostname = url.hostname;
|
const hostname = url.hostname;
|
||||||
|
|
||||||
|
|
||||||
|
console.log(`Running scan on ${url}`)
|
||||||
|
|
||||||
async function tryFetch(path) {
|
async function tryFetch(path) {
|
||||||
const target = url.origin + path;
|
const target = url.origin + path;
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await fetch(target, { redirect: "manual" });
|
response = await fetch(target);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return response.status === 200 ? target : null;
|
return [200, 301].includes(response.status) ? response : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
env: await tryFetch("/.env"),
|
env: await tryFetch("/.env"),
|
||||||
git: await tryFetch("/.git"),
|
git: await tryFetch("/.git")
|
||||||
dsstore: await tryFetch("/.DS_Store"),
|
|
||||||
config: await tryFetch("/.config"),
|
|
||||||
svn: await tryFetch("/.svn"),
|
|
||||||
npm: await tryFetch("/.npm"),
|
|
||||||
hg: await tryFetch("/.hg"),
|
|
||||||
docker: await tryFetch("/.docker"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stored = await browser.storage.local.get("entries");
|
const stored = await browser.storage.local.get("entries");
|
||||||
const entries = stored.entries || [];
|
const entries = stored.entries || [];
|
||||||
|
|
||||||
for (const [key, foundPath] of Object.entries(results)) {
|
for (const [key, response] of Object.entries(results)) {
|
||||||
if (!foundPath) continue;
|
if (!response) continue;
|
||||||
|
|
||||||
|
const validated = await validatePathResponse(response.url, response);
|
||||||
|
|
||||||
|
if (!validated.ok) {
|
||||||
|
console.log(`Rejected ${key}: ${validated.reason}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Hit! A hidden file was found: ${key}`)
|
||||||
|
|
||||||
entries.push({
|
entries.push({
|
||||||
domainField: hostname,
|
domainField: hostname,
|
||||||
pathField: foundPath,
|
pathField: response.url,
|
||||||
type: key
|
type: key
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await browser.storage.local.set({ entries });
|
await browser.storage.local.set({ entries });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((msg) => {
|
browser.runtime.onMessage.addListener((msg) => {
|
||||||
if (msg.type === "runOnce") {
|
if (msg.type === "runOnce") {
|
||||||
runSingleScan();
|
browser.tabs.query({ active: true, currentWindow: true }).then(([tab]) => {
|
||||||
|
if (tab) runSingleScan(tab);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Check if returned value is a file/path based on heuristics
|
||||||
|
async function validatePathResponse(url, response) {
|
||||||
|
|
||||||
|
const text = await response.text().catch(() => "");
|
||||||
|
const contentType = response.headers.get("content-type")?.toLowerCase() || "";
|
||||||
|
const status = response.status;
|
||||||
|
|
||||||
|
const fileIndicators = [
|
||||||
|
"application/octet-stream",
|
||||||
|
"application/x-git",
|
||||||
|
"text/plain",
|
||||||
|
"application/env",
|
||||||
|
];
|
||||||
|
|
||||||
|
const isLikelyFile = fileIndicators.some(type => contentType.includes(type));
|
||||||
|
if (isLikelyFile && status === 200) {
|
||||||
|
return { ok: true, type: "file", reason: "content-type indicates file" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const directoryKeywords = /(Index of|Directory listing|Parent Directory)/i;
|
||||||
|
const errorKeywords = /(oops|error|not found|forbidden|unauthorized|denied)/i;
|
||||||
|
|
||||||
|
const looksLikeDirectory = directoryKeywords.test(text);
|
||||||
|
const looksLikeErrorPage = errorKeywords.test(text);
|
||||||
|
|
||||||
|
if (looksLikeDirectory && !looksLikeErrorPage) {
|
||||||
|
return { ok: true, type: "directory", reason: "directory listing heuristics" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.endsWith("/.git")) {
|
||||||
|
const head = await tryFetchInternal(url + "/HEAD");
|
||||||
|
if (head?.status === 200) {
|
||||||
|
const headText = await head.text().catch(() => "");
|
||||||
|
if (/ref:/.test(headText)) {
|
||||||
|
return { ok: true, type: "directory", reason: ".git HEAD file found" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 404 || looksLikeErrorPage) {
|
||||||
|
return { ok: false, reason: "soft 404 or error page" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: false, reason: "did not match any valid directory/file heuristics" };
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user