diff --git a/README.md b/README.md index 10c2e02..699e060 100755 --- a/README.md +++ b/README.md @@ -97,9 +97,8 @@ Env: dev **Medium** ``` /swagger-ui.html /swagger.json /api-docs -/openapi.json /robots.txt /.well-known/security.txt -/web.config /.htaccess /Dockerfile -/docker-compose.yml +/openapi.json /web.config /.htaccess +/Dockerfile /docker-compose.yml ``` @@ -144,15 +143,29 @@ Access settings via the **gear icon** in the popup: ## Testing -A test environment is included to verify the extension works correctly: +A dynamic test server is included to verify the extension works correctly: ```bash cd test/ -./start-server-macos.command # macOS (opens browser automatically) +python3 server.py # Recommended: Dynamic server +``` + +Alternative (static server, limited functionality): +```bash +./start-server-macos.command # macOS ./start-server.sh # Linux/other ``` -This starts a local server on port 9000 with fake sensitive files and debug endpoints. +The dynamic server (`server.py`) serves different content based on debug params/headers: +- **Normal request**: Clean production page (no sensitive data) +- **With debug params**: Debug page with exposed credentials, stack traces, etc. + +This mimics real-world behavior where debug endpoints only expose sensitive data when triggered. + +Test URLs: +- `http://localhost:9000/` — Normal page +- `http://localhost:9000/?debug=1` — Debug mode triggered +- `http://localhost:9000/.env` — Sensitive path ## Technical Details @@ -163,6 +176,16 @@ This starts a local server on port 9000 with fake sensitive files and debug endp ## Changelog +### v2.0.6 +- **Reduced false positives on dynamic sites** (login pages, news sites, etc.) + - Redirect detection: Filters paths that redirect to catch-all destinations + - Natural variance measurement: Detects highly dynamic sites + - Smart mode now requires clear evidence (debug indicators or status changes) + - Debug indicators must be NEW (not present in original response) +- **Fixed path detection on non-standard ports** (e.g., localhost:9000) +- **New dynamic test server** (`test/server.py`) that mimics real-world behavior +- Improved soft-404 detection (content length comparison) + ### v2.0.0 - Complete rewrite with Manifest V3 - Multi-factor detection engine diff --git a/background.js b/background.js index e27da57..ea4cb92 100644 --- a/background.js +++ b/background.js @@ -1,6 +1,9 @@ /** - * debugHunter v2.0.0 - Background Service Worker + * debugHunter v2.0.6 - Background Service Worker * Multi-factor detection with configurable comparison strategies + * - Added redirect detection to filter false positives on paths + * - Added natural variance measurement to filter false positives on dynamic sites + * - Require variance check for all detections without debug indicators */ import { stringSimilarity } from './similarity.js'; @@ -180,6 +183,7 @@ const debugHeaders = [ async function getSettings() { const result = await chrome.storage.sync.get([ + 'enabled', 'detectionMode', 'requireDebugIndicators', 'detectStatusChanges', @@ -195,6 +199,7 @@ async function getSettings() { ]); return { + enabled: result.enabled !== false, // Enabled by default detectionMode: result.detectionMode || 'smart', requireDebugIndicators: result.requireDebugIndicators !== false, detectStatusChanges: result.detectStatusChanges !== false, @@ -293,6 +298,11 @@ function containsDebugIndicators(text) { return { found: false, level: null }; } +function getLevelPriority(level) { + const priorities = { critical: 4, high: 3, medium: 2, low: 1 }; + return priorities[level] || 0; +} + function extractInterestingHeaders(response) { const found = {}; for (const header of debugHeaders) { @@ -323,7 +333,7 @@ function compareHeaders(original, modified) { // MULTI-FACTOR COMPARISON // ============================================================================ -async function analyzeResponseDifference(originalResponse, modifiedResponse, originalText, modifiedText, settings) { +async function analyzeResponseDifference(originalResponse, modifiedResponse, originalText, modifiedText, settings, naturalVariance = null) { const result = { isDifferent: false, confidence: 0, @@ -331,6 +341,7 @@ async function analyzeResponseDifference(originalResponse, modifiedResponse, ori severity: 'low', debugIndicators: null, headerChanges: [], + requiresVarianceCheck: false, // Flag to trigger control request verification }; // 1. Status code change detection @@ -360,15 +371,28 @@ async function analyzeResponseDifference(originalResponse, modifiedResponse, ori } } - // 3. Content length difference + // 3. Content length difference (variance-aware) const lengthDiff = Math.abs(modifiedText.length - originalText.length); - if (lengthDiff >= settings.minLengthDiff) { + // If we know the site's natural variance, only count if difference EXCEEDS natural variance + const isLengthWithinVariance = naturalVariance && lengthDiff <= naturalVariance.lengthDiff * 1.2; + + if (!isLengthWithinVariance && lengthDiff >= settings.minLengthDiff) { result.reasons.push(`Content length diff: ${lengthDiff} bytes`); result.confidence += Math.min(lengthDiff / 100, 25); } - // 4. Debug indicator detection - const debugCheck = containsDebugIndicators(modifiedText); + // 4. Debug indicator detection - only count if NEW (not present in original) + const debugCheckModified = containsDebugIndicators(modifiedText); + const debugCheckOriginal = containsDebugIndicators(originalText); + + // Only consider debug indicators that are NEW (caused by the param/header) + // If the same level of indicator exists in original, it's not caused by our test + const debugCheck = { + found: debugCheckModified.found && (!debugCheckOriginal.found || + getLevelPriority(debugCheckModified.level) > getLevelPriority(debugCheckOriginal.level)), + level: debugCheckModified.level, + }; + if (debugCheck.found) { result.debugIndicators = debugCheck; result.reasons.push(`Debug indicators found: ${debugCheck.level}`); @@ -379,7 +403,7 @@ async function analyzeResponseDifference(originalResponse, modifiedResponse, ori else if (debugCheck.level === 'medium' && !['critical', 'high'].includes(result.severity)) result.severity = 'medium'; } - // 5. Similarity check (after filtering dynamic content) + // 5. Similarity check (after filtering dynamic content, variance-aware) let originalFiltered = originalText; let modifiedFiltered = modifiedText; @@ -389,11 +413,25 @@ async function analyzeResponseDifference(originalResponse, modifiedResponse, ori } const similarity = stringSimilarity.compareTwoStrings(originalFiltered, modifiedFiltered); - if (similarity < settings.similarityThreshold) { + + // If we know the site's natural variance, only count if similarity is WORSE than natural variance + // E.g., if site naturally has 92% similarity between requests, only flag if this request is < 90% + const isSimilarityWithinVariance = naturalVariance && similarity >= naturalVariance.similarity - 0.02; + + if (!isSimilarityWithinVariance && similarity < settings.similarityThreshold) { result.reasons.push(`Similarity: ${(similarity * 100).toFixed(1)}%`); result.confidence += (1 - similarity) * 30; } + // If we have confidence but NO debug indicators, always verify with variance check + // This prevents false positives on dynamic sites (login pages, news sites, etc.) + if (!naturalVariance && !debugCheck.found && result.confidence > 0) { + result.requiresVarianceCheck = true; + } + + // Only critical/high/medium indicators count as significant (low like "Warning:" can appear in normal pages) + const hasSignificantDebugIndicators = debugCheck.found && ['critical', 'high', 'medium'].includes(debugCheck.level); + // Determine if response is different based on mode switch (settings.detectionMode) { case 'aggressive': @@ -413,12 +451,26 @@ async function analyzeResponseDifference(originalResponse, modifiedResponse, ori case 'smart': default: - // Multi-factor: needs significant confidence - // If debug indicators found, lower threshold - if (settings.requireDebugIndicators) { - result.isDifferent = result.confidence >= 40 && debugCheck.found; + // Smart mode: require clear evidence to avoid false positives on dynamic sites + const hasStatusBypass = originalResponse.status === 403 && modifiedResponse.status === 200; + const hasServerError = modifiedResponse.status >= 500; + // Check if content is significantly different (not just dynamic variation) + const isSignificantlyDifferent = similarity < 0.70; + // Debug indicators in modified response (even if also in original) + const hasAnyDebugIndicators = debugCheckModified.found && ['critical', 'high', 'medium'].includes(debugCheckModified.level); + + if (hasStatusBypass || hasServerError) { + // Clear signal - status change is strong evidence + result.isDifferent = true; + } else if (hasSignificantDebugIndicators) { + // NEW debug indicators found - report + result.isDifferent = result.confidence >= 40; + } else if (hasAnyDebugIndicators && isSignificantlyDifferent) { + // Debug indicators exist AND content is very different - likely more debug info triggered + result.isDifferent = true; } else { - result.isDifferent = result.confidence >= 50; + // No clear evidence - don't report to avoid FPs on dynamic sites + result.isDifferent = false; } break; } @@ -595,6 +647,65 @@ async function getUrlBaseline(url) { } } +// ============================================================================ +// NATURAL VARIANCE MEASUREMENT (for dynamic sites) +// ============================================================================ + +const varianceCache = new Map(); + +async function measureNaturalVariance(url, baselineText, settings, useRandomParam = false) { + // Cache key includes whether we're measuring with params + const cacheKey = useRandomParam ? `${url}#withParam` : url; + + // Check cache first (valid for 2 minutes) + if (varianceCache.has(cacheKey)) { + const cached = varianceCache.get(cacheKey); + if (Date.now() - cached.timestamp < 120000) { + return cached.variance; + } + } + + try { + // For params, measure variance by adding a random param to see how the site responds + // This catches sites that return different content when ANY query param is present + let controlUrl = url; + if (useRandomParam) { + const randomParam = `_rnd${Math.random().toString(36).substring(7)}`; + const urlObj = new URL(url); + urlObj.searchParams.set(randomParam, '1'); + controlUrl = urlObj.href; + } + + const controlResponse = await rateLimitedFetch(controlUrl); + const controlText = await controlResponse.text(); + + // Filter dynamic content before comparison + let baselineFiltered = baselineText; + let controlFiltered = controlText; + + if (settings.filterDynamicContent) { + baselineFiltered = filterDynamicContent(baselineText, settings.dynamicPatterns); + controlFiltered = filterDynamicContent(controlText, settings.dynamicPatterns); + } + + // Calculate natural variance between baseline and control + const naturalSimilarity = stringSimilarity.compareTwoStrings(baselineFiltered, controlFiltered); + const naturalLengthDiff = Math.abs(controlText.length - baselineText.length); + + const variance = { + similarity: naturalSimilarity, + lengthDiff: naturalLengthDiff, + // Site is "highly dynamic" if requests differ significantly + isHighlyDynamic: naturalSimilarity < 0.95, + }; + + varianceCache.set(cacheKey, { variance, timestamp: Date.now() }); + return variance; + } catch (e) { + return null; + } +} + // ============================================================================ // PARAMETER CHECKING (uses cached baseline) // ============================================================================ @@ -607,7 +718,6 @@ function appendParam(url, param) { async function checkParams(url, baseline = null) { const settings = await getSettings(); - const allParams = [...debugParams.high, ...debugParams.medium]; try { // Use provided baseline or fetch new one @@ -622,6 +732,9 @@ async function checkParams(url, baseline = null) { ...debugParams.medium.map(p => ({ ...p, confidence: 'medium' })), ]; + // Track if we've measured variance for this URL (lazy - only when needed) + let measuredVariance = null; + for (const param of sortedParams) { const modifiedUrl = appendParam(url, param); @@ -629,12 +742,31 @@ async function checkParams(url, baseline = null) { const modifiedResponse = await rateLimitedFetch(modifiedUrl); const modifiedText = await modifiedResponse.text(); - const analysis = await analyzeResponseDifference( + // First analysis without variance + let analysis = await analyzeResponseDifference( baseline.mockResponse, modifiedResponse, baseline.text, modifiedText, - settings + settings, + measuredVariance ); + // If flagged but needs variance verification (no debug indicators found) + if (analysis.isDifferent && analysis.requiresVarianceCheck && !measuredVariance) { + // Measure variance with a random param to see how site responds to ANY query param + // This catches sites that return different content when params are present (vs absent) + measuredVariance = await measureNaturalVariance(url, baseline.text, settings, true); + + if (measuredVariance) { + // Re-analyze with variance knowledge - always re-check, not just for highly dynamic sites + analysis = await analyzeResponseDifference( + baseline.mockResponse, modifiedResponse, + baseline.text, modifiedText, + settings, + measuredVariance + ); + } + } + if (analysis.isDifferent) { await addFinding('params', { url: modifiedUrl, @@ -670,6 +802,9 @@ async function checkHeaders(url, baseline = null) { } if (!baseline) return; + // Track if we've measured variance for this URL (lazy - only when needed) + let measuredVariance = null; + for (const header of customHeaders) { try { const headers = new Headers(); @@ -678,12 +813,30 @@ async function checkHeaders(url, baseline = null) { const modifiedResponse = await rateLimitedFetch(url, { headers }); const modifiedText = await modifiedResponse.text(); - const analysis = await analyzeResponseDifference( + // First analysis without variance + let analysis = await analyzeResponseDifference( baseline.mockResponse, modifiedResponse, baseline.text, modifiedText, - settings + settings, + measuredVariance ); + // If flagged but needs variance verification (no debug indicators found) + if (analysis.isDifferent && analysis.requiresVarianceCheck && !measuredVariance) { + // Measure natural variance with a control request + measuredVariance = await measureNaturalVariance(url, baseline.text, settings); + + if (measuredVariance) { + // Re-analyze with variance knowledge - always re-check, not just for highly dynamic sites + analysis = await analyzeResponseDifference( + baseline.mockResponse, modifiedResponse, + baseline.text, modifiedText, + settings, + measuredVariance + ); + } + } + if (analysis.isDifferent) { await addFinding('headers', { url, @@ -710,6 +863,24 @@ async function checkHeaders(url, baseline = null) { // Cache for domain baselines and soft-404 fingerprints const domainCache = new Map(); +// Normalize redirect URL for comparison (resolves relative URLs, removes trailing slashes) +function normalizeRedirectUrl(location, baseUrl) { + try { + const resolved = new URL(location, baseUrl); + // Return pathname without trailing slash for consistent comparison + return resolved.pathname.replace(/\/$/, '') || '/'; + } catch (e) { + return location; + } +} + +// Check if a redirect is just URL normalization (trailing slash, case change) +function isNormalizationRedirect(originalPath, redirectPath) { + const normalizedOriginal = originalPath.replace(/\/$/, '').toLowerCase(); + const normalizedRedirect = redirectPath.replace(/\/$/, '').toLowerCase(); + return normalizedOriginal === normalizedRedirect; +} + async function getDomainBaseline(baseUrl) { if (domainCache.has(baseUrl)) { const cached = domainCache.get(baseUrl); @@ -723,18 +894,32 @@ async function getDomainBaseline(baseUrl) { const baseResponse = await rateLimitedFetch(baseUrl); const baseText = await baseResponse.text(); - // Get soft-404 fingerprint (request a random non-existent path) + // Get soft-404 fingerprint and catch-all redirect (request a random non-existent path) const randomPath = `/${Math.random().toString(36).substring(7)}-${Date.now()}`; let soft404Fingerprint = null; let soft404Length = 0; + let catchAllRedirect = null; try { - const soft404Response = await rateLimitedFetch(baseUrl + randomPath); - const soft404Text = await soft404Response.text(); + // Use redirect: 'manual' to detect catch-all redirects + const soft404Response = await rateLimitedFetch(baseUrl + randomPath, { redirect: 'manual' }); + + // Check if the random path redirects somewhere (catch-all redirect pattern) + if (soft404Response.status >= 300 && soft404Response.status < 400) { + const location = soft404Response.headers.get('location'); + if (location) { + // Normalize the redirect URL for comparison + catchAllRedirect = normalizeRedirectUrl(location, baseUrl); + } + } + + // For fingerprinting, follow the redirect to get actual content + const finalResponse = await rateLimitedFetch(baseUrl + randomPath); + const soft404Text = await finalResponse.text(); soft404Length = soft404Text.length; // Create a fingerprint based on content structure, not exact content soft404Fingerprint = { - status: soft404Response.status, + status: finalResponse.status, length: soft404Text.length, hasTitle: //i.test(soft404Text), isSoft404: isSoft404(soft404Text), @@ -748,6 +933,7 @@ async function getDomainBaseline(baseUrl) { baseLength: baseText.length, soft404Fingerprint, soft404Length, + catchAllRedirect, timestamp: Date.now(), }; @@ -761,35 +947,57 @@ async function getDomainBaseline(baseUrl) { function matchesSoft404(response, text, fingerprint) { if (!fingerprint) return false; - // If it returned the same status as our random 404 probe + // If it returned the same status as our random 404 probe (non-200) if (fingerprint.status === response.status && fingerprint.status !== 200) { return true; } - // If content length is very similar to soft-404 (within 10%) const lengthDiff = Math.abs(text.length - fingerprint.length); - if (lengthDiff < fingerprint.length * 0.1 && fingerprint.isSoft404) { + const lengthRatio = lengthDiff / fingerprint.length; + + // If content length is nearly identical (within 3%), very likely the same page + // This catches soft-404s that return 200 without "404" text + if (lengthRatio < 0.03) { + return true; + } + + // If content length is similar (within 10%) AND has soft-404 indicators + if (lengthRatio < 0.1 && fingerprint.isSoft404) { return true; } return false; } -async function checkPathWithHead(baseUrl, path, settings) { +async function checkPathWithHead(baseUrl, path, settings, catchAllRedirect = null) { const testUrl = baseUrl + path; try { - // First, try HEAD request to check existence (saves bandwidth) - const headResponse = await rateLimitedFetch(testUrl, { method: 'HEAD' }); + // First, try HEAD request with redirect: manual to detect catch-all redirects + const headResponse = await rateLimitedFetch(testUrl, { method: 'HEAD', redirect: 'manual' }); + + // Check for redirects (3xx status codes) + if (headResponse.status >= 300 && headResponse.status < 400) { + const location = headResponse.headers.get('location'); + if (location && catchAllRedirect) { + const redirectPath = normalizeRedirectUrl(location, baseUrl); + // Only skip if it redirects to the SAME place as the random path probe + if (redirectPath === catchAllRedirect && !isNormalizationRedirect(path, redirectPath)) { + return null; // Catch-all redirect - false positive + } + } + // For other redirects, continue and follow them (could be legit /admin -> /admin/login) + } - // Only proceed if status indicates potential content - if (headResponse.status === 200 || headResponse.status === 403) { + // Only proceed if status indicates potential content (or redirect that we'll follow) + if (headResponse.status === 200 || headResponse.status === 403 || + (headResponse.status >= 300 && headResponse.status < 400)) { const contentLength = parseInt(headResponse.headers.get('content-length') || '0'); - // Skip if too small (likely empty or error) - if (contentLength > 0 && contentLength < 30) return null; + // Skip if too small (likely empty or error) - but only for 200 responses + if (headResponse.status === 200 && contentLength > 0 && contentLength < 30) return null; - // Now do full GET to analyze content + // Now do full GET to analyze content (this will follow redirects) const response = await rateLimitedFetch(testUrl); if (response.status === 200) { return { response, url: testUrl }; @@ -798,6 +1006,20 @@ async function checkPathWithHead(baseUrl, path, settings) { } catch (e) { // Try direct GET if HEAD fails (some servers don't support HEAD) try { + // Check for catch-all redirect first + const getResponse = await rateLimitedFetch(testUrl, { redirect: 'manual' }); + + if (getResponse.status >= 300 && getResponse.status < 400 && catchAllRedirect) { + const location = getResponse.headers.get('location'); + if (location) { + const redirectPath = normalizeRedirectUrl(location, baseUrl); + if (redirectPath === catchAllRedirect && !isNormalizationRedirect(path, redirectPath)) { + return null; // Catch-all redirect - false positive + } + } + } + + // Follow the redirect and get final content const response = await rateLimitedFetch(testUrl); if (response.status === 200) { return { response, url: testUrl }; @@ -815,7 +1037,7 @@ async function checkPaths(url) { try { const urlObj = new URL(url); - const baseUrl = `${urlObj.protocol}//${urlObj.hostname}`; + const baseUrl = `${urlObj.protocol}//${urlObj.host}`; // Use .host to include port // Get cached domain baseline const baseline = await getDomainBaseline(baseUrl); @@ -835,7 +1057,7 @@ async function checkPaths(url) { const results = await Promise.all( batch.map(({ path, severity }) => - checkPathWithHead(baseUrl, path, settings).then(result => + checkPathWithHead(baseUrl, path, settings, baseline.catchAllRedirect).then(result => result ? { ...result, severity, path } : null ) ) @@ -882,6 +1104,11 @@ async function checkPaths(url) { // ============================================================================ async function scanUrl(url) { + const settings = await getSettings(); + + // Check if scanning is enabled globally + if (!settings.enabled) return; + if (!await shouldScanUrl(url)) return; const domain = new URL(url).hostname; @@ -889,8 +1116,6 @@ async function scanUrl(url) { console.log(`%c[debugHunter] Scanning: ${url}`, 'background: #9b59b6; color: white; padding: 2px 6px; border-radius: 3px'); - const settings = await getSettings(); - // Get baseline once for params and headers (saves 1 request) const baseline = await getUrlBaseline(url); @@ -945,6 +1170,24 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'getScanStatus': sendResponse(scanStatus); break; + case 'getEnabled': { + const enabledSettings = await getSettings(); + sendResponse({ enabled: enabledSettings.enabled }); + break; + } + case 'setEnabled': { + await chrome.storage.sync.set({ enabled: message.enabled }); + // Update badge to reflect state + if (!message.enabled) { + chrome.action.setBadgeText({ text: 'OFF' }); + chrome.action.setBadgeBackgroundColor({ color: '#6e7681' }); + } else { + const currentFindings = await getFindings(); + updateBadge(currentFindings); + } + sendResponse({ enabled: message.enabled }); + break; + } default: sendResponse({ error: 'Unknown action' }); } @@ -953,13 +1196,25 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { }); chrome.runtime.onInstalled.addListener(async () => { - const findings = await getFindings(); - updateBadge(findings); + const settings = await getSettings(); + if (!settings.enabled) { + chrome.action.setBadgeText({ text: 'OFF' }); + chrome.action.setBadgeBackgroundColor({ color: '#6e7681' }); + } else { + const findings = await getFindings(); + updateBadge(findings); + } }); chrome.runtime.onStartup.addListener(async () => { - const findings = await getFindings(); - updateBadge(findings); + const settings = await getSettings(); + if (!settings.enabled) { + chrome.action.setBadgeText({ text: 'OFF' }); + chrome.action.setBadgeBackgroundColor({ color: '#6e7681' }); + } else { + const findings = await getFindings(); + updateBadge(findings); + } }); -console.log('[debugHunter] Service worker v2.0.0 - Multi-factor detection'); +console.log('[debugHunter] Service worker v2.0.1 - Multi-factor detection'); diff --git a/manifest.json b/manifest.json index a5f3257..f6ad4db 100755 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "debugHunter", - "version": "2.0.0", + "version": "2.0.6", "description": "Discover hidden debugging parameters, headers, and sensitive paths to access dev/sandbox/pre-production environments", "options_page": "options.html", "icons": { diff --git a/options.html b/options.html index 52caa63..d1ae3d9 100755 --- a/options.html +++ b/options.html @@ -618,7 +618,7 @@ <h2>Whitelist</h2> <!-- Footer --> <div class="footer"> - <p>debugHunter v2.0 • <a href="https://github.com/devploit/debugHunter" target="_blank">GitHub</a> • Exposing what should stay hidden</p> + <p>debugHunter v2.0.1 • <a href="https://github.com/devploit/debugHunter" target="_blank">GitHub</a> • Exposing what should stay hidden</p> </div> </div> diff --git a/options.js b/options.js index 57da819..94d5bf4 100755 --- a/options.js +++ b/options.js @@ -1,5 +1,5 @@ /** - * debugHunter v2.0.0 - Options Script + * debugHunter v2.0.1 - Options Script * Advanced settings management */ diff --git a/popup.html b/popup.html index a041754..9850802 100755 --- a/popup.html +++ b/popup.html @@ -109,6 +109,72 @@ color: var(--critical); } + /* Toggle Switch */ + .toggle-switch { + display: flex; + align-items: center; + gap: 6px; + margin-right: 8px; + padding-right: 12px; + border-right: 1px solid var(--border-color); + } + + .toggle-label { + font-size: 11px; + color: var(--text-secondary); + text-transform: uppercase; + font-weight: 500; + } + + .toggle-container { + position: relative; + width: 36px; + height: 20px; + } + + .toggle-input { + opacity: 0; + width: 0; + height: 0; + } + + .toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--bg-hover); + border-radius: 10px; + transition: all 0.2s ease; + } + + .toggle-slider:before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: var(--text-muted); + border-radius: 50%; + transition: all 0.2s ease; + } + + .toggle-input:checked + .toggle-slider { + background-color: var(--low); + } + + .toggle-input:checked + .toggle-slider:before { + transform: translateX(16px); + background-color: white; + } + + .toggle-input:focus + .toggle-slider { + box-shadow: 0 0 0 2px var(--accent); + } + /* Status Bar */ .status-bar { display: none; @@ -584,9 +650,16 @@ <i class="fas fa-bug"></i> </div> <span class="logo-text">debugHunter</span> - <span class="logo-version">v2.0</span> + <span class="logo-version">v2.0.1</span> </div> <div class="controls"> + <div class="toggle-switch"> + <span class="toggle-label">Scan</span> + <label class="toggle-container"> + <input type="checkbox" class="toggle-input" id="scan-toggle" checked> + <span class="toggle-slider"></span> + </label> + </div> <button class="control-btn" id="options-link" title="Settings"> <i class="fas fa-cog"></i> </button> diff --git a/popup.js b/popup.js index 0e0d043..70c40ab 100755 --- a/popup.js +++ b/popup.js @@ -1,5 +1,5 @@ /** - * debugHunter v2.0.0 - Popup Script + * debugHunter v2.0.1 - Popup Script * Features: Diff viewer, Severity stats, Scan status */ @@ -31,6 +31,14 @@ async function getScanStatus() { return await sendMessage('getScanStatus'); } +async function getEnabled() { + return await sendMessage('getEnabled'); +} + +async function setEnabled(enabled) { + return await sendMessage('setEnabled', { enabled }); +} + // ============================================================================ // SEVERITY STATS // ============================================================================ @@ -360,9 +368,19 @@ async function updateUI() { // EVENT HANDLERS // ============================================================================ -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener('DOMContentLoaded', async () => { updateUI(); + // Initialize scan toggle state + const scanToggle = document.getElementById('scan-toggle'); + const enabledState = await getEnabled(); + scanToggle.checked = enabledState.enabled; + + // Scan toggle handler + scanToggle.addEventListener('change', async (e) => { + await setEnabled(e.target.checked); + }); + // Category collapse toggle document.querySelectorAll('.category-header').forEach((header) => { header.addEventListener('click', () => { diff --git a/test/server.py b/test/server.py new file mode 100644 index 0000000..14b3a06 --- /dev/null +++ b/test/server.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +""" +debugHunter Test Server - Dynamic version +Serves different content based on debug params/headers +""" + +import http.server +import socketserver +import os +from urllib.parse import urlparse, parse_qs + +PORT = 9000 +DIRECTORY = os.path.dirname(os.path.abspath(__file__)) + +# Debug params that trigger debug mode +DEBUG_PARAMS = [ + '_debug', 'debug', 'debug_mode', 'XDEBUG_SESSION_START', 'XDEBUG_SESSION', + 'debugbar', 'profiler', 'trace', 'verbose', 'show_errors', 'display_errors', + 'dev_mode', 'phpinfo', 'error_reporting', 'env', 'environment', 'staging', + 'beta', 'internal', 'test', 'admin' +] + +# Debug headers that trigger debug mode +DEBUG_HEADERS = [ + 'x-debug', 'x-forwarded-host', 'x-forwarded-for', 'x-original-url', + 'x-env', 'env', 'x-real-ip' +] + +class DebugHunterHandler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=DIRECTORY, **kwargs) + + def do_GET(self): + parsed = urlparse(self.path) + query_params = parse_qs(parsed.query) + + # Check for debug params + detected_params = [] + for param in DEBUG_PARAMS: + if param in query_params: + detected_params.append(f"{param}={query_params[param][0]}") + + # Check for debug headers + detected_headers = [] + for header in DEBUG_HEADERS: + value = self.headers.get(header) + if value: + detected_headers.append(f"{header}: {value}") + + is_debug_mode = len(detected_params) > 0 or len(detected_headers) > 0 + + # Serve dynamic index for root path + if parsed.path == '/' or parsed.path == '/index.html': + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + + html = self.generate_html(is_debug_mode, detected_params, detected_headers) + self.wfile.write(html.encode()) + return + + # Serve static files for other paths + super().do_GET() + + def do_HEAD(self): + parsed = urlparse(self.path) + + if parsed.path == '/' or parsed.path == '/index.html': + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + return + + super().do_HEAD() + + def generate_html(self, is_debug_mode, detected_params, detected_headers): + if is_debug_mode: + return self.generate_debug_html(detected_params, detected_headers) + else: + return self.generate_normal_html() + + def generate_normal_html(self): + return '''<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Production App + + + +
+

Welcome to Our App

+
Production Mode - Everything is secure
+

This is a normal production page with no sensitive information.

+
+ +''' + + def generate_debug_html(self, detected_params, detected_headers): + params_str = ', '.join(detected_params) if detected_params else 'none' + headers_str = ', '.join(detected_headers) if detected_headers else 'none' + + return f''' + + + + DEBUG MODE ACTIVE + + + +
+

DEBUG MODE ACTIVE

+
WARNING: Sensitive information exposed!
+ +
+
[Detected Debug Params]
+
{params_str}
+
+ +
+
[Detected Debug Headers]
+
{headers_str}
+
+ +
+
[Environment Variables]
+
+DB_HOST=localhost
+DB_NAME=production_db
+DB_PASSWORD=super_secret_password_123!
+API_KEY=sk-1234567890abcdef1234567890abcdef
+AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
+SECRET_KEY=my-super-secret-application-key
+      
+
+ +
+
[Server Configuration]
+
+PHP Version: PHP/8.2.0
+Server: Apache/2.4.52
+Document Root: /var/www/html
+Server User: www-data
+Debug Mode: true
+      
+
+ +
+
[Stack Trace]
+
+Fatal error: Uncaught Exception in /var/www/html/app/core.php:142
+Stack trace:
+#0 /var/www/html/app/core.php(142): Database->connect()
+#1 /var/www/html/app/bootstrap.php(28): Application->init()
+#2 /var/www/html/index.php(5): require_once('/var/www/html/...')
+#3 {{main}}
+      
+
+
+ +''' + +def run_server(): + print("") + print(" " + "=" * 60) + print(" debugHunter Test Server (Dynamic)") + print(" " + "=" * 60) + print(f" Server: http://localhost:{PORT}") + print("") + print(" Test URLs:") + print(f" Main page: http://localhost:{PORT}/") + print(f" With debug: http://localhost:{PORT}/?debug=1") + print(f" With env: http://localhost:{PORT}/?env=dev") + print("") + print(" Sensitive paths (debugHunter should detect these):") + print(f" /.env Credentials, API keys") + print(f" /.git/config Git repository info") + print(f" /config.json Database passwords") + print(f" /phpinfo.php PHP configuration") + print(f" /debug Debug console") + print("") + print(" Press Ctrl+C to stop the server") + print("") + + with socketserver.TCPServer(("", PORT), DebugHunterHandler) as httpd: + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nServer stopped.") + +if __name__ == "__main__": + run_server()