diff --git a/auto.html b/auto.html deleted file mode 100644 index da24866..0000000 --- a/auto.html +++ /dev/null @@ -1 +0,0 @@ -Auto Start \ No newline at end of file diff --git a/dashboard.css b/css/dashboard.css similarity index 100% rename from dashboard.css rename to css/dashboard.css diff --git a/html/auto.html b/html/auto.html new file mode 100644 index 0000000..4536b60 --- /dev/null +++ b/html/auto.html @@ -0,0 +1 @@ +Auto Start \ No newline at end of file diff --git a/dashboard.html b/html/dashboard.html similarity index 94% rename from dashboard.html rename to html/dashboard.html index d791750..a382a42 100644 --- a/dashboard.html +++ b/html/dashboard.html @@ -3,7 +3,7 @@ ReqRes DevTools Lite v3 - +
@@ -28,7 +28,7 @@ 0 dipilih - +
@@ -75,6 +75,6 @@ - + \ No newline at end of file diff --git a/auto.js b/js/auto.js similarity index 100% rename from auto.js rename to js/auto.js diff --git a/bg.js b/js/bg.js similarity index 99% rename from bg.js rename to js/bg.js index e96b08e..4753079 100644 --- a/bg.js +++ b/js/bg.js @@ -19,7 +19,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { function broadcast(event, data){ chrome.runtime.sendMessage({ __RRSTREAM:true, event, data }); } async function openOrFocusDashboard(){ - const url = chrome.runtime.getURL('dashboard.html'); + const url = chrome.runtime.getURL('html/dashboard.html'); const tabs = await chrome.tabs.query({}); const found = tabs.find(t => t.url === url); if (found) { diff --git a/dashboard.js b/js/dashboard.js similarity index 69% rename from dashboard.js rename to js/dashboard.js index 8be4da4..f885f5b 100644 --- a/dashboard.js +++ b/js/dashboard.js @@ -8,7 +8,7 @@ const btnClear = $("#btnClear"); const btnSelectMode = $("#btnSelectMode"); const btnSelectAll = $("#btnSelectAll"); const btnClearSelection = $("#btnClearSelection"); -const btnExportSelectedZIP = $("#btnExportSelectedZIP"); +const btnExportSelectedHAR = $("#btnExportSelectedHAR"); const selCount = $("#selCount"); const modeCbs = Array.from(document.querySelectorAll("input.mode")); @@ -180,7 +180,7 @@ function updateSelUi(){ const count = selectedIds.size; selCount.textContent = `${count} dipilih`; btnClearSelection.disabled = count === 0; - btnExportSelectedZIP.disabled = count === 0; + btnExportSelectedHAR.disabled = count === 0; btnSelectAll.disabled = !selectMode; masterSel && (masterSel.checked = false); } @@ -473,172 +473,74 @@ function guessExt(mime, enc) { return enc === 'base64' ? '.bin' : '.txt'; } -// Export ZIP (Readable) -btnExportSelectedZIP.addEventListener('click', async () => { +// Export HAR +btnExportSelectedHAR.addEventListener('click', async () => { const selected = rows.filter(r => selectedIds.has(r.id)).sort((a,b)=>(a.seq||0)-(b.seq||0)); - const pad = (n)=>String(n).padStart(5,'0'); - const enc = new TextEncoder(); - const files = []; - // README & index - const readme = `# Export Req/Res (Readable) -Organized by Domain > Request. -- 00_summary.txt : Metadata -- 01_req_headers.txt -- 02_req_body.(json/txt) -- 03_res_headers.txt -- 04_res_body.(json/html/txt/bin) -`; - files.push({ name: "README.md", data: enc.encode(readme) }); - const csvHeader = "seq,timestamp,method,status,domain,path,mime,size,url\n"; - let csv = csvHeader; - let md = "| seq | method | status | domain | path | mime | size |\n|---:|:--|:--:|:--|:--|:--|--:|\n"; - for (const r of selected){ - const urlObj = new URL(r.url); - const host = sanitize(urlObj.hostname); - const path = sanitize(urlObj.pathname).slice(-50).replace(/^_+/, ''); - const folderName = `${pad(r.seq||0)}_${sanitize(r.method)}_${path}`; - const base = `${host}/${folderName}`; - - // Meta - const meta = [ - `URL: ${r.url}`, - `Method: ${r.method}`, - `Status: ${r.status} ${r.statusText||""}`, - `MIME: ${r.mimeType||"-"}`, - `Size: ${r.bodySize??0}`, - `Started: ${r.startedDateTime||""}`, - `Time(ms): ${Math.round((r.time||0)*1000)}`, - `Category: ${guessKind(r)}`, - ].join('\n'); - files.push({ name: `${base}/00_summary.txt`, data: enc.encode(meta) }); - - // Headers - files.push({ name: `${base}/01_req_headers.txt`, data: enc.encode(formatHeaders(r.requestHeaders)) }); - files.push({ name: `${base}/03_res_headers.txt`, data: enc.encode(formatHeaders(r.responseHeaders)) }); - - // Request Body - let reqExt = guessExt((r.requestHeaders||[]).find(h=>h.name.toLowerCase()==='content-type')?.value||'text/plain', 'text'); - let reqBody = r.requestBodyText || ''; - if (reqExt === '.json') { try { reqBody = JSON.stringify(JSON.parse(reqBody), null, 2); } catch {} } - files.push({ name: `${base}/02_req_body${reqExt}`, data: enc.encode(reqBody) }); - - // Response Body - let resExt = guessExt(r.mimeType, r.responseBodyEncoding); - let resBodyData; - if (r.responseBodyEncoding === 'base64') { - resBodyData = b64toBytes(r.responseBodyRaw||''); - if (resExt === '.json') { - try { - const text = new TextDecoder().decode(resBodyData); - const json = JSON.stringify(JSON.parse(text), null, 2); - resBodyData = enc.encode(json); - } catch {} - } - } else { - let text = r.responseBodyRaw || ''; - if (resExt === '.json') { try { text = JSON.stringify(JSON.parse(text), null, 2); } catch {} } - resBodyData = enc.encode(text); - } - files.push({ name: `${base}/04_res_body${resExt}`, data: resBodyData }); - csv += `${r.seq||0},${csvCell(r.startedDateTime)},${csvCell(r.method)},${r.status||0},${csvCell(host)},${csvCell(urlObj.pathname)},${csvCell(r.mimeType)},${r.bodySize||0},${csvCell(r.url)}\n`; - md += `| ${r.seq||0} | ${r.method||''} | ${r.status||0} | ${host} | ${urlObj.pathname} | ${r.mimeType||''} | ${r.bodySize||0} |\n`; - } - files.push({ name: "index.csv", data: enc.encode(csv) }); - files.push({ name: "index.md", data: enc.encode(md) }); - - const zipBlob = buildZip(files); - const url = URL.createObjectURL(zipBlob); const a=document.createElement('a'); a.href=url; a.download='reqres_readable.zip'; a.click(); URL.revokeObjectURL(url); -}); + const entries = selected.map(r => { + return { + startedDateTime: r.startedDateTime || new Date().toISOString(), + time: Math.round((r.time||0)*1000) || 0, + request: { + method: r.method, + url: r.url, + httpVersion: "HTTP/1.1", + headers: r.requestHeaders || [], + queryString: [], + cookies: [], + headersSize: -1, + bodySize: -1, + postData: r.requestBodyText ? { + mimeType: (r.requestHeaders||[]).find(h=>h.name.toLowerCase()==='content-type')?.value || "text/plain", + text: r.requestBodyText + } : undefined + }, + response: { + status: r.status || 0, + statusText: r.statusText || "", + httpVersion: "HTTP/1.1", + headers: r.responseHeaders || [], + cookies: [], + content: { + size: r.bodySize || 0, + mimeType: r.mimeType || "x-unknown", + text: r.responseBodyRaw || "", + encoding: r.responseBodyEncoding === "base64" ? "base64" : undefined + }, + redirectURL: "", + headersSize: -1, + bodySize: r.bodySize || 0 + }, + cache: {}, + timings: { + send: 0, + wait: Math.round((r.time||0)*1000) || 0, + receive: 0 + } + }; + }); -// minimal ZIP (store) builder -function buildZip(files){ - const enc = new TextEncoder(); - let fileData = []; let central = []; let offset = 0; const now = new Date(); - const dosTime = ((now.getHours()<<11) | (now.getMinutes()<<5) | (Math.floor(now.getSeconds()/2))) & 0xFFFF; - const dosDate = (((now.getFullYear()-1980)<<9) | ((now.getMonth()+1)<<5) | now.getDate()) & 0xFFFF; - for (const f of files){ - const nameBytes = enc.encode(f.name); - const data = f.data instanceof Uint8Array ? f.data : new Uint8Array(f.data||[]); - const crc = crc32(data); - const local = new Uint8Array(30 + nameBytes.length + data.length); - const dv = new DataView(local.buffer); - dv.setUint32(0, 0x04034b50, true); // local header sig - dv.setUint16(4, 20, true); // version needed - dv.setUint16(6, 0, true); // flags - dv.setUint16(8, 0, true); // method: store - dv.setUint16(10, dosTime, true); - dv.setUint16(12, dosDate, true); - dv.setUint32(14, crc>>>0, true); - dv.setUint32(18, data.length, true); - dv.setUint32(22, data.length, true); - dv.setUint16(26, nameBytes.length, true); - dv.setUint16(28, 0, true); // extra len - local.set(nameBytes, 30); - local.set(data, 30 + nameBytes.length); - fileData.push(local); - // central directory entry - const centralEntry = new Uint8Array(46 + nameBytes.length); - const cv = new DataView(centralEntry.buffer); - cv.setUint32(0, 0x02014b50, true); - cv.setUint16(4, 20, true); // version made by - cv.setUint16(6, 20, true); // version needed - cv.setUint16(8, 0, true); // flags - cv.setUint16(10, 0, true); // method - cv.setUint16(12, dosTime, true); - cv.setUint16(14, dosDate, true); - cv.setUint32(16, crc>>>0, true); - cv.setUint32(20, data.length, true); - cv.setUint32(24, data.length, true); - cv.setUint16(28, nameBytes.length, true); - cv.setUint16(30, 0, true); // extra len - cv.setUint16(32, 0, true); // comment len - cv.setUint16(34, 0, true); // disk number start - cv.setUint16(36, 0, true); // internal attrs - cv.setUint32(38, 0, true); // external attrs - cv.setUint32(42, offset, true); // local header offset - centralEntry.set(nameBytes, 46); - central.push(centralEntry); - offset += local.length; - } - // end of central directory - const sizeOfCentral = central.reduce((a,b)=>a+b.length, 0); - const offsetOfCentral = offset; - const end = new Uint8Array(22); - const ev = new DataView(end.buffer); - ev.setUint32(0, 0x06054b50, true); - ev.setUint16(4, 0, true); // disk - ev.setUint16(6, 0, true); // start disk - const count = files.length; - ev.setUint16(8, count, true); - ev.setUint16(10, count, true); - ev.setUint32(12, sizeOfCentral, true); - ev.setUint32(16, offsetOfCentral, true); - ev.setUint16(20, 0, true); // comment len - return new Blob([...fileData, ...central, end], { type: "application/zip" }); -} -// CRC32 -const CRC_TABLE = (() => { - let c; - let table = []; - for (let n = 0; n < 256; n++) { - c = n; - for (let k = 0; k < 8; k++) { - c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); + const harLog = { + log: { + version: "1.2", + creator: { + name: "ReqRes DevTools Lite", + version: "1.5.0" + }, + entries: entries } - table[n] = c >>> 0; - } - return table; -})(); + }; + + const jsonStr = JSON.stringify(harLog, null, 2); + const blob = new Blob([jsonStr], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'reqres_export.har'; + a.click(); + URL.revokeObjectURL(url); +}); -function crc32(u8) { - let c = 0 ^ (-1); - for (let i = 0; i < u8.length; i++) { - c = (c >>> 8) ^ CRC_TABLE[(c ^ u8[i]) & 0xFF]; - } - return (c ^ (-1)) >>> 0; -} -// end ZIP chrome.runtime.onMessage.addListener((msg) => { if (!msg || !msg.__RRSTREAM) return; diff --git a/manifest.json b/manifest.json index a63f3c7..3f0dfc7 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "ReqRes DevTools Lite (Auto) v3", "version": "1.5.0", - "description": "Auto-capture. Anti-refresh log. Mode filter XHR/JS/CSS/Img/etc. Export ZIP (Readable).", + "description": "Auto-capture. Anti-refresh log. Mode filter XHR/JS/CSS/Img/etc. Export HAR.", "minimum_chrome_version": "105", "permissions": [ "debugger", @@ -21,12 +21,12 @@ "128": "icons/icon128.png" }, "background": { - "service_worker": "bg.js", + "service_worker": "js/bg.js", "type": "module" }, "action": { "default_title": "Start capture & open dashboard", - "default_popup": "auto.html", + "default_popup": "html/auto.html", "default_icon": { "16": "icons/icon16.png" } diff --git a/reproduce_vulnerability.js b/reproduce_vulnerability.js deleted file mode 100644 index 94b1090..0000000 --- a/reproduce_vulnerability.js +++ /dev/null @@ -1,94 +0,0 @@ -const assert = require('assert'); - -// Mock helpers from dashboard.js -function humanSize(bytes){ if(bytes==null||isNaN(bytes))return "-"; const u=["B","KB","MB","GB"]; let i=0,n=Math.max(0,bytes);while(n>=1024&&i ({ - tagName: tag.toUpperCase(), - appendChild: function(child) { this.children.push(child); }, - children: [], - classList: { - toggle: function(cls, val) { - if (val) this.classes.add(cls); else this.classes.delete(cls); - }, - add: function(cls) { this.classes.add(cls); }, - delete: function(cls) { this.classes.delete(cls); } - }, - classes: new Set(), - addEventListener: () => {}, - prepend: function(child) { this.children.unshift(child); } - }) -}; - -// The vulnerable function -function updateRowContent(tr, r){ - const selTd = mockDocument.createElement("td"); selTd.className = "selcol" + (selectMode ? "" : " hidden"); - if(selectMode) { - const cb = mockDocument.createElement("input"); cb.type="checkbox"; cb.checked = selectedIds.has(r.id); - cb.addEventListener("click", (ev)=>{ ev.stopPropagation(); if (cb.checked) selectedIds.add(r.id); else selectedIds.delete(r.id); updateSelUi(); }); - selTd.appendChild(cb); - } - - tr.innerHTML = ` - ${nameFromUrl(r.url)} - ${r.method||"-"} - ${r.status||"-"} - ${r.protocol||"-"} - ${r.remoteIPAddress||"-"} - ${r.mimeType||"-"} - ${guessKind(r)} - ${humanSize(r.bodySize)} - ${Math.round((r.time||0)*1000)}`; - tr.prepend(selTd); -} - -// Test case -const tr = { - innerHTML: '', - children: [], - prepend: function(child) { this.children.unshift(child); } -}; - -const maliciousRequest = { - url: 'http://example.com/normal', - method: '', - status: 200, - protocol: 'HTTP/1.1', - remoteIPAddress: '127.0.0.1', - mimeType: 'text/html', - bodySize: 100, - time: 0.1 -}; - -console.log("Running reproduction test..."); -updateRowContent(tr, maliciousRequest); - -console.log("Generated innerHTML:\n", tr.innerHTML); - -if (tr.innerHTML.includes('')) { - console.log("\n[!] VULNERABILITY REPRODUCED: HTML injection detected in r.method!"); -} else { - console.log("\n[?] Vulnerability NOT reproduced in r.method."); -} - -const maliciousUrlRequest = { - url: 'javascript:alert(1)', - method: 'GET', - status: 200, - protocol: 'HTTP/1.1', - remoteIPAddress: '127.0.0.1', - mimeType: 'text/html', - bodySize: 100, - time: 0.1 -}; - -updateRowContent(tr, maliciousUrlRequest); -if (tr.innerHTML.includes('title="javascript:alert(1)"')) { - console.log("[!] VULNERABILITY REPRODUCED: HTML injection detected in r.url (title attribute)!"); -} diff --git a/reproduce_zip_vulnerability.js b/reproduce_zip_vulnerability.js deleted file mode 100644 index 05da447..0000000 --- a/reproduce_zip_vulnerability.js +++ /dev/null @@ -1,53 +0,0 @@ -const sanitize = (s) => (s || '').replace(/[^a-z0-9._-]+/gi, '_'); - -function test() { - const r = { - seq: 1, - method: '../../evil', - url: 'http://example.com/foo' - }; - - const urlObj = new URL(r.url); - const host = sanitize(urlObj.hostname); - const path = sanitize(urlObj.pathname).slice(-50).replace(/^_+/, ''); - const folderName = `${String(r.seq).padStart(5, '0')}_${r.method}_${path}`; - const base = `${host}/${folderName}`; - - const fileName = `${base}/01_req_headers.txt`; - console.log("Generated fileName:", fileName); - - if (fileName.includes('../')) { - console.log("[!] VULNERABILITY REPRODUCED: Path traversal detected in ZIP file name!"); - } else { - console.log("[?] Vulnerability NOT reproduced."); - } -} - -test(); - -function test_hostname() { - const r = { - seq: 1, - method: 'GET', - url: 'http://../foo' - }; - - try { - const urlObj = new URL(r.url); - const host = sanitize(urlObj.hostname); - const path = sanitize(urlObj.pathname).slice(-50).replace(/^_+/, ''); - const folderName = `${String(r.seq).padStart(5, '0')}_${r.method}_${path}`; - const base = `${host}/${folderName}`; - - const fileName = `${base}/01_req_headers.txt`; - console.log("Generated fileName (with dotdot hostname):", fileName); - - if (fileName.includes('../') || fileName.startsWith('../')) { - console.log("[!] VULNERABILITY REPRODUCED: Path traversal detected in ZIP file name via hostname!"); - } - } catch (e) { - console.log("URL parsing failed for 'http://../foo':", e.message); - } -} - -test_hostname(); diff --git a/tests/bench_headers.js b/tests/bench_headers.js index 21705bb..5edce45 100644 --- a/tests/bench_headers.js +++ b/tests/bench_headers.js @@ -2,7 +2,7 @@ const fs = require('fs'); const vm = require('vm'); const assert = require('assert'); -const code = fs.readFileSync('bg.js', 'utf8'); +const code = fs.readFileSync('js/bg.js', 'utf8'); // Extraction logic based on project memory: brace-counting or regex function extractFunction(name, source) { diff --git a/verify_fix.js b/verify_fix.js deleted file mode 100644 index 3f98b8c..0000000 --- a/verify_fix.js +++ /dev/null @@ -1,75 +0,0 @@ -const assert = require('assert'); - -function sanitize(s){ - let t = (s || '').replace(/[^a-z0-9._-]+/gi, '_'); - while (t.includes('..')) t = t.replace(/\.\./g, '__'); - return t || '_'; -} - -function csvSafe(s){ - if (typeof s !== 'string') return s; - return (['=', '+', '-', '@'].some(c => s.startsWith(c))) ? "'" + s : s; -} - -function test_zip_path_traversal() { - console.log("Testing ZIP path traversal fix..."); - const r = { - seq: 1, - method: '../../evil', - url: 'http://example.com/foo' - }; - - const urlObj = new URL(r.url); - const host = sanitize(urlObj.hostname); - const path = sanitize(urlObj.pathname).slice(-50).replace(/^_+/, ''); - const folderName = `${String(r.seq).padStart(5, '0')}_${sanitize(r.method)}_${path}`; - const base = `${host}/${folderName}`; - - const fileName = `${base}/01_req_headers.txt`; - console.log("Generated fileName:", fileName); - - assert(!fileName.includes('../'), "Path traversal pattern detected!"); - console.log("[PASS] ZIP path traversal fix verified."); -} - -function test_zip_hostname_traversal() { - console.log("Testing ZIP hostname traversal fix..."); - const r = { - seq: 1, - method: 'GET', - url: 'http://../foo' - }; - - const urlObj = new URL(r.url); - const host = sanitize(urlObj.hostname); - const path = sanitize(urlObj.pathname).slice(-50).replace(/^_+/, ''); - const folderName = `${String(r.seq).padStart(5, '0')}_${sanitize(r.method)}_${path}`; - const base = `${host}/${folderName}`; - - const fileName = `${base}/01_req_headers.txt`; - console.log("Generated fileName:", fileName); - - assert(!fileName.includes('../'), "Path traversal pattern detected in hostname!"); - console.log("[PASS] ZIP hostname traversal fix verified."); -} - -function test_csv_injection() { - console.log("Testing CSV injection fix..."); - const maliciousInputs = ['=1+1', '+A1', '-1', '@A1']; - for (const input of maliciousInputs) { - const safe = csvSafe(input); - console.log(`${input} -> ${safe}`); - assert(safe.startsWith("'"), `CSV injection not neutralized for ${input}`); - } - console.log("[PASS] CSV injection fix verified."); -} - -try { - test_zip_path_traversal(); - test_zip_hostname_traversal(); - test_csv_injection(); - console.log("\nAll security verification tests passed!"); -} catch (e) { - console.error("\n[FAIL] Security verification failed:", e.message); - process.exit(1); -}