mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
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
349 lines
13 KiB
JavaScript
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);
|
|
};
|
|
})();
|