Files
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

349 lines
13 KiB
JavaScript

/**
* api-recon coverage-mode preload script — reference template
*
* ⚠ Not a turnkey product: adjust for the target site before injecting.
* Common edits: loginPathRe, stubs, neutralize.fields/success, apiPattern, mockTier, forward
*
* document-start injection (CDP addScriptToEvaluateOnNewDocument or a userscript)
* CONFIG fields should be kept in sync with recon/config.json
*/
(function () {
'use strict';
const CONFIG = {
loginPathRe: /\/(login|signin)(\/|$|\?)/i,
mockTier: 'L1+L2',
forward: true,
neutralize: {
fields: ['response_code', 'code', 'errno', 'ret', 'status'],
success: 0,
flags: { success: true, message: 'ok' },
},
stubs: [],
apiPattern: /\/(api|apis|v\d+|dev|internal|graphql)\//i,
apiFallbackRe: /^\/(api|apis|v\d+|dev|internal)\//i,
// API discovery enhancements: fetch/XHR hooks, request-header and response recording
recordDetail: true, // record method/url/headers/body/response in detail
respMax: 600,
extractUrlsFromResponse: true, // dig nested URLs out of JSON responses
neutralizeVueRouter: true, // neutralize Vue beforeEach/push login redirects
observe: {
storageReads: false, // observe localStorage.getItem (helps confirm session key names)
cookieReads: false, // observe document.cookie reads
xhrHeaders: true, // record XHR setRequestHeader
},
};
window.__API_RECON_PRELOAD__ = true;
window.__API_RECON_LOG__ = window.__API_RECON_LOG__ || new Set();
window.__API_RECON_DETAIL__ = window.__API_RECON_DETAIL__ || [];
window.__API_RECON_ROUTES__ = window.__API_RECON_ROUTES__ || new Set();
window.__API_RECON_OBSERVE__ = window.__API_RECON_OBSERVE__ || { storage: [], headers: [] };
function trunc(s, n) {
n = n || CONFIG.respMax || 600;
s = String(s == null ? '' : s);
return s.length > n ? s.slice(0, n) + '…' : s;
}
function extractApiUrlsFromText(text) {
if (!CONFIG.extractUrlsFromResponse || !text) return;
const re = /["'](\/(?:api|apis|v\d+|dev|internal|graphql)[^"'`\\s]*)["'`]/gi;
let m;
while ((m = re.exec(text))) {
const p = m[1].split('?')[0];
window.__API_RECON_LOG__.add('GET ' + p);
}
const broad = /\/api\/[a-zA-Z0-9_./-]+/g;
while ((m = broad.exec(text))) {
const p = m[0].split('?')[0];
if (!/\.(svg|png|jpg|gif|ico)$/i.test(p)) window.__API_RECON_LOG__.add('GET ' + p);
}
}
function recordApi(url, method) {
const p = String(url || '').split('?')[0];
if (CONFIG.apiPattern.test(p)) {
window.__API_RECON_LOG__.add((method || 'GET').toUpperCase() + ' ' + p);
}
}
function recordApiDetail(entry) {
recordApi(entry.url, entry.method);
if (!CONFIG.recordDetail) return;
window.__API_RECON_DETAIL__.push(entry);
if (entry.responseBody) extractApiUrlsFromText(entry.responseBody);
}
function blocked(url) {
return url && CONFIG.loginPathRe.test(String(url));
}
// --- redirect neutralization ---
(function neutralizeNativeNavigation() {
const rawAssign = Location.prototype.assign;
const rawReplace = Location.prototype.replace;
Location.prototype.assign = function (url) {
if (blocked(url)) return;
return rawAssign.call(this, url);
};
Location.prototype.replace = function (url) {
if (blocked(url)) return;
return rawReplace.call(this, url);
};
const rawPush = history.pushState;
const rawRep = history.replaceState;
history.pushState = function (s, t, url) {
if (blocked(url)) return;
return rawPush.apply(this, arguments);
};
history.replaceState = function (s, t, url) {
if (blocked(url)) return;
return rawRep.apply(this, arguments);
};
const hrefDesc = Object.getOwnPropertyDescriptor(Location.prototype, 'href');
if (hrefDesc && hrefDesc.set) {
const nativeSet = hrefDesc.set;
Object.defineProperty(Location.prototype, 'href', {
configurable: true,
enumerable: hrefDesc.enumerable,
get: hrefDesc.get,
set(url) {
if (blocked(url)) return;
return nativeSet.call(this, url);
},
});
}
window.close = function () {};
})();
// --- Vue Router login-redirect neutralization ---
function neutralizeVueRouter() {
if (!CONFIG.neutralizeVueRouter) return;
try {
const el = document.querySelector('[data-v-app]');
const app = (el && el.__vue_app__) || window.__VUE__;
if (!app) return;
const router = app.config && app.config.globalProperties && app.config.globalProperties.$router;
if (!router) return;
router.beforeEach(function (to, from, next) { next(); });
if (router.beforeResolve) router.beforeResolve(function (to, from, next) { next(); });
function wrapNav(fn) {
return function (loc) {
const path = typeof loc === 'string' ? loc : (loc && (loc.path || loc.fullPath)) || '';
if (blocked(path)) return Promise.resolve();
return fn.apply(this, arguments);
};
}
router.push = wrapNav(router.push.bind(router));
router.replace = wrapNav(router.replace.bind(router));
if (router.getRoutes) {
router.getRoutes().forEach(function (r) {
if (r.path) window.__API_RECON_ROUTES__.add(r.path);
});
}
} catch (e) { /* ignore */ }
}
function runPostLoadHooks() {
neutralizeVueRouter();
// business-layer navigation functions (goPage / navigateTo etc.)
['goPage', 'navigateTo', 'jumpTo', 'redirectTo'].forEach(function (name) {
if (typeof window[name] !== 'function' || window[name].__apiReconWrapped) return;
const raw = window[name];
window[name] = function () {
const arg = arguments[0];
const path = typeof arg === 'string' ? arg : (arg && arg.path) || '';
if (blocked(path)) return;
return raw.apply(this, arguments);
};
window[name].__apiReconWrapped = true;
});
}
document.addEventListener('DOMContentLoaded', runPostLoadHooks);
window.addEventListener('load', runPostLoadHooks);
// --- Observation hooks: help discover session key names and request headers (optional; storage/cookie off by default) ---
if (CONFIG.observe && CONFIG.observe.storageReads) {
const rawGet = Storage.prototype.getItem;
Storage.prototype.getItem = function (key) {
window.__API_RECON_OBSERVE__.storage.push({ type: 'getItem', key: key, at: location.pathname });
return rawGet.apply(this, arguments);
};
}
if (CONFIG.observe && CONFIG.observe.cookieReads) {
const cookieDesc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
if (cookieDesc && cookieDesc.get) {
const nativeGet = cookieDesc.get;
Object.defineProperty(document, 'cookie', {
configurable: true,
get: function () {
window.__API_RECON_OBSERVE__.storage.push({ type: 'cookieRead', at: location.pathname });
return nativeGet.call(this);
},
set: cookieDesc.set,
});
}
}
// --- Mock helpers ---
const NEGATIVE_RE = /not\s*logged\s*in|not\s*authorized|authorized|not\s*login|unauthorized|forbidden/i;
const tier = CONFIG.mockTier || 'L1+L2';
function patchJsonBody(text) {
if (!tier.includes('L2')) return text;
try {
const j = JSON.parse(text);
if (j && typeof j === 'object') {
const msg = String(j.message || j.msg || '');
for (const f of CONFIG.neutralize.fields) {
if (f in j && j[f] !== CONFIG.neutralize.success && NEGATIVE_RE.test(msg)) {
j[f] = CONFIG.neutralize.success;
}
}
Object.assign(j, CONFIG.neutralize.flags || {});
if (j.data == null) j.data = {};
return JSON.stringify(j);
}
} catch (e) { /* ignore */ }
return text;
}
function lookupPrecise(url) {
if (!tier.includes('L1')) return null;
const p = String(url || '');
for (const s of CONFIG.stubs) {
if (s._re ? s._re.test(p) : new RegExp(s.match).test(p)) return s.body;
}
return null;
}
function fallbackBody(url) {
const p = String(url).split('?')[0];
if (/\/(list|search|query|page|all|tree|nodes|options)/i.test(p)) {
return { response_code: 0, data: [] };
}
if (/\/(get|detail|info|config|status)/i.test(p)) {
return { response_code: 0, data: {} };
}
return { response_code: 0, data: {}, success: true };
}
function matchMock(url) {
const precise = lookupPrecise(url);
if (precise) return precise;
if (tier.includes('L3') && CONFIG.apiFallbackRe.test(String(url || '').split('?')[0])) {
return fallbackBody(url);
}
return null;
}
CONFIG.stubs.forEach(function (s) {
if (s.match && !s._re) s._re = new RegExp(s.match);
});
// --- fetch Hook ---
const rawFetch = window.fetch;
window.fetch = async function (input, init) {
const url = typeof input === 'string' ? input : (input && input.url) || '';
const method = ((init && init.method) || 'GET').toUpperCase();
const reqHeaders = (init && init.headers) || {};
const reqBody = init && init.body ? trunc(init.body) : null;
const mock = matchMock(url);
if (mock && !CONFIG.forward) {
recordApiDetail({ method: method, url: url, reqHeaders: reqHeaders, reqBody: reqBody, status: 200, responseBody: JSON.stringify(mock), source: 'mock' });
return new Response(JSON.stringify(mock), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
const resp = await rawFetch.apply(this, arguments);
const clone = resp.clone();
let text = '';
try { text = await clone.text(); } catch (e) { /* ignore */ }
recordApiDetail({
method: method, url: url, reqHeaders: reqHeaders, reqBody: reqBody,
status: resp.status, responseBody: trunc(text), source: 'fetch',
});
if (!tier.includes('L2') && !mock) return resp;
const patched = patchJsonBody(text);
if (patched !== text) {
return new Response(patched, {
status: resp.status,
statusText: resp.statusText,
headers: resp.headers,
});
}
return resp;
};
// --- XHR Hook ---
const rawOpen = XMLHttpRequest.prototype.open;
const rawSend = XMLHttpRequest.prototype.send;
const rawSetHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.open = function (method, url) {
this.__apiReconUrl = url;
this.__apiReconMethod = method;
this.__apiReconHeaders = {};
return rawOpen.apply(this, arguments);
};
if (CONFIG.observe && CONFIG.observe.xhrHeaders) {
XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
if (!this.__apiReconHeaders) this.__apiReconHeaders = {};
this.__apiReconHeaders[name] = value;
window.__API_RECON_OBSERVE__.headers.push({ name: name, url: this.__apiReconUrl });
return rawSetHeader.apply(this, arguments);
};
}
XMLHttpRequest.prototype.send = function (body) {
const xhr = this;
const url = xhr.__apiReconUrl || '';
const method = (xhr.__apiReconMethod || 'GET').toUpperCase();
const mock = matchMock(url);
if (mock && !CONFIG.forward) {
const bodyStr = JSON.stringify(mock);
recordApiDetail({ method: method, url: url, reqHeaders: xhr.__apiReconHeaders, reqBody: trunc(body), status: 200, responseBody: bodyStr, source: 'mock' });
setTimeout(function () {
Object.defineProperty(xhr, 'readyState', { configurable: true, get: function () { return 4; } });
Object.defineProperty(xhr, 'status', { configurable: true, get: function () { return 200; } });
Object.defineProperty(xhr, 'responseText', { configurable: true, get: function () { return bodyStr; } });
xhr.onreadystatechange && xhr.onreadystatechange();
xhr.onload && xhr.onload();
}, 0);
return;
}
const orig = xhr.onreadystatechange;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
recordApiDetail({
method: method, url: url, reqHeaders: xhr.__apiReconHeaders,
reqBody: trunc(body), status: xhr.status,
responseBody: trunc(xhr.responseText), source: 'xhr',
});
if (tier.includes('L2') && xhr.responseText) {
try {
const patched = patchJsonBody(xhr.responseText);
if (patched !== xhr.responseText) {
Object.defineProperty(xhr, 'responseText', { configurable: true, get: function () { return patched; } });
}
} catch (e) { /* ignore */ }
}
}
orig && orig.apply(this, arguments);
};
return rawSend.apply(this, arguments);
};
})();