Skip to content

Commit 37f84df

Browse files
committed
Merge branch 'fix/s3-region-full-url'
2 parents c73febd + 16138d7 commit 37f84df

1 file changed

Lines changed: 86 additions & 18 deletions

File tree

internal/api/ui/securitylab/index.html

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,10 @@ <h1 class="title">Security Lab</h1>
321321
</div>
322322
</div>
323323
<div class="card">
324-
<div class="k">Generated Payloads</div>
324+
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:6px">
325+
<div class="k" style="margin:0">Generated Payloads</div>
326+
<button class="copy-bulk-btn" id="ssrfCopyAll">Copy All</button>
327+
</div>
325328
<div id="ssrfResults" class="list" style="max-height: 500px;"></div>
326329
</div>
327330
</section>
@@ -1237,6 +1240,43 @@ <h1 class="title">Security Lab</h1>
12371240
});
12381241

12391242
// SSRF Logic
1243+
// Robust clipboard copy that also works over plain HTTP (the Clipboard API is
1244+
// only available in secure contexts, so the dashboard needs an execCommand fallback).
1245+
function ssrfCopyText(text) {
1246+
if (navigator.clipboard && window.isSecureContext) {
1247+
return navigator.clipboard.writeText(text);
1248+
}
1249+
return new Promise((resolve, reject) => {
1250+
try {
1251+
const ta = document.createElement('textarea');
1252+
ta.value = text;
1253+
ta.setAttribute('readonly', '');
1254+
ta.style.position = 'fixed';
1255+
ta.style.top = '-1000px';
1256+
ta.style.opacity = '0';
1257+
document.body.appendChild(ta);
1258+
ta.select();
1259+
const ok = document.execCommand('copy');
1260+
document.body.removeChild(ta);
1261+
ok ? resolve() : reject(new Error('copy command rejected'));
1262+
} catch (e) { reject(e); }
1263+
});
1264+
}
1265+
1266+
function ssrfFlashBtn(btn, msg, restore) {
1267+
if (!btn) return;
1268+
btn.textContent = msg;
1269+
setTimeout(() => { btn.textContent = restore; }, 1600);
1270+
}
1271+
1272+
// Filtered render state, kept so Copy All / Copy group operate on exactly what's shown.
1273+
let ssrfFilteredUrls = [];
1274+
let ssrfGroups = [];
1275+
1276+
function ssrfCopyAllLabel() {
1277+
return ssrfFilteredUrls.length ? `Copy All (${ssrfFilteredUrls.length})` : 'Copy All';
1278+
}
1279+
12401280
function renderSSRF() {
12411281
const allowed = $('ssrfAllowed').value || 'reservation.walibi.nl';
12421282
const attacker = $('ssrfAttacker').value || 'evil.com';
@@ -1250,47 +1290,75 @@ <h1 class="title">Security Lab</h1>
12501290
}
12511291

12521292
let html = '';
1253-
let count = 0;
1293+
ssrfFilteredUrls = [];
1294+
ssrfGroups = [];
12541295

12551296
window.SSRF_PAYLOADS.forEach(group => {
12561297
let groupHtml = '';
1257-
let groupCount = 0;
1258-
1298+
const groupUrls = [];
1299+
12591300
(group.payloads || []).forEach(p => {
12601301
if (filter) {
12611302
const desc = (p.description || '').toLowerCase();
12621303
const tags = (p.tags || []).join(' ').toLowerCase();
12631304
if (!desc.includes(filter) && !tags.includes(filter)) return;
12641305
}
12651306

1266-
let pl = p.payload || '';
1267-
pl = pl.replace(/<allowed>/g, allowed).replace(/<attacker>/g, attacker);
1268-
1269-
let fullUrl = (p.prefix || '') + pl + (p.suffix || '');
1270-
let displayUrl = escapeHTML(fullUrl);
1271-
1307+
let pl = (p.payload || '').replace(/<allowed>/g, allowed).replace(/<attacker>/g, attacker);
1308+
const fullUrl = (p.prefix || '') + pl + (p.suffix || '');
1309+
const idx = ssrfFilteredUrls.length;
1310+
ssrfFilteredUrls.push(fullUrl);
1311+
groupUrls.push(fullUrl);
1312+
1313+
// data-i indexes ssrfFilteredUrls; copy is handled by delegation below so we
1314+
// never inline-escape payloads (which broke on quotes / backslashes / newlines).
12721315
groupHtml += `<div class="item" style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;">
12731316
<div style="word-break: break-all;">
1274-
<code style="color:var(--text); font-size:14px; display:block; margin-bottom:4px">${displayUrl}</code>
1317+
<code style="color:var(--text); font-size:14px; display:block; margin-bottom:4px">${escapeHTML(fullUrl)}</code>
12751318
<span class="muted" style="font-size:11px">${escapeHTML(p.description || 'No description')}</span>
12761319
</div>
1277-
<button class="btn ghost" style="padding:4px 8px; font-size:11px" onclick="navigator.clipboard.writeText('${escapeHTML(fullUrl).replace(/'/g, "\\'")}')">Copy</button>
1320+
<button class="btn ghost ssrf-copy-one" data-i="${idx}" style="padding:4px 8px; font-size:11px; flex-shrink:0">Copy</button>
12781321
</div>`;
1279-
groupCount++;
1280-
count++;
12811322
});
12821323

1283-
if (groupCount > 0) {
1284-
html += `<div style="padding: 8px; background: rgba(123,139,255,.05); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 10;">
1285-
<b style="color:var(--accent)">${escapeHTML(group.name)}</b> <span class="muted">(${groupCount})</span>
1324+
if (groupUrls.length) {
1325+
const gi = ssrfGroups.length;
1326+
ssrfGroups.push({ name: group.name, urls: groupUrls });
1327+
html += `<div style="padding: 8px; background: rgba(123,139,255,.05); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 10; display:flex; justify-content:space-between; align-items:center; gap:10px;">
1328+
<span><b style="color:var(--accent)">${escapeHTML(group.name)}</b> <span class="muted">(${groupUrls.length})</span></span>
1329+
<button class="btn ghost ssrf-copy-group" data-g="${gi}" style="padding:3px 8px; font-size:10px; flex-shrink:0">Copy group</button>
12861330
</div>` + groupHtml;
12871331
}
12881332
});
12891333

12901334
resultsEl.innerHTML = html || '<div class="item muted">No payloads found.</div>';
1291-
statsEl.textContent = `${count} payload(s) generated.`;
1335+
statsEl.textContent = `${ssrfFilteredUrls.length} payload(s) generated.`;
1336+
$('ssrfCopyAll').textContent = ssrfCopyAllLabel();
12921337
}
12931338

1339+
// Single delegated handler for per-payload and per-group copy (survives re-renders).
1340+
$('ssrfResults').addEventListener('click', (e) => {
1341+
const one = e.target.closest('.ssrf-copy-one');
1342+
if (one) {
1343+
const url = ssrfFilteredUrls[+one.dataset.i];
1344+
if (url != null) ssrfCopyText(url).then(() => ssrfFlashBtn(one, 'Copied', 'Copy'));
1345+
return;
1346+
}
1347+
const grp = e.target.closest('.ssrf-copy-group');
1348+
if (grp) {
1349+
const urls = (ssrfGroups[+grp.dataset.g] || {}).urls || [];
1350+
if (urls.length) ssrfCopyText(urls.join('\n')).then(() => ssrfFlashBtn(grp, `Copied ${urls.length}`, 'Copy group'));
1351+
}
1352+
});
1353+
1354+
$('ssrfCopyAll').addEventListener('click', function () {
1355+
if (!ssrfFilteredUrls.length) return;
1356+
ssrfCopyText(ssrfFilteredUrls.join('\n')).then(() => {
1357+
this.textContent = `Copied ${ssrfFilteredUrls.length}!`;
1358+
setTimeout(() => { this.textContent = ssrfCopyAllLabel(); }, 1800);
1359+
});
1360+
});
1361+
12941362
$('ssrfGenerate').addEventListener('click', renderSSRF);
12951363
$('ssrfFilter').addEventListener('input', renderSSRF);
12961364
if (window.SSRF_PAYLOADS) renderSSRF();

0 commit comments

Comments
 (0)