// Enable, Idsable automatic listener and the message listener for it function enableListener() { if (!browser.tabs.onUpdated.hasListener(tabUpdateListener)) { browser.tabs.onUpdated.addListener(tabUpdateListener); } } function disableListener() { if (browser.tabs.onUpdated.hasListener(tabUpdateListener)) { 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) => { if (msg.type === "toggleListener") { if (msg.enabled) enableListener(); else disableListener(); } }); // 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]; const url = new URL(tab.url); const hostname = url.hostname; console.log(`Running scan on ${url}`) async function tryFetch(path) { const target = url.origin + path; let response; try { response = await fetch(target); } catch { return null; } return [200, 301].includes(response.status) ? response : null; } const results = { env: await tryFetch("/.env"), git: await tryFetch("/.git") }; const stored = await browser.storage.local.get("entries"); const entries = stored.entries || []; for (const [key, response] of Object.entries(results)) { 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({ domainField: hostname, pathField: response.url, type: key }); } await browser.storage.local.set({ entries }); } browser.runtime.onMessage.addListener((msg) => { if (msg.type === "runOnce") { 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" }; }