Skip to content

Instantly share code, notes, and snippets.

@kidGodzilla
Last active December 18, 2025 17:49
Show Gist options
  • Select an option

  • Save kidGodzilla/ad41c5240117bacb15da89effde39f81 to your computer and use it in GitHub Desktop.

Select an option

Save kidGodzilla/ad41c5240117bacb15da89effde39f81 to your computer and use it in GitHub Desktop.
Detect tampering with fetch/XMLHttpRequest
(function () {
'use strict';
// ================= CONFIG =================
const CONFIG = {
enabled: true,
checkIntervalMs: 1500,
// reporting
reportEnabled: true,
reportUrl: 'https://your-endpoint.example.com/runtime-integrity',
reportSampleRate: 1.0, // 0–1
// logging
logEnabled: true,
// detection
suspicionThreshold: 3,
// targets
monitorFetch: true,
monitorXHR: true
};
// optional remote override (flat only)
if (window.__INTEGRITY_CONFIG__) {
try {
Object.assign(CONFIG, window.__INTEGRITY_CONFIG__);
} catch {}
}
if (!CONFIG.enabled) return;
// ================= HARD REFERENCES =================
const fnToString = Function.prototype.toString;
const warn = console.warn.bind(console);
// capture fetch once; even if tainted, it stays stable
const safeFetch = window.fetch;
try { Object.freeze(safeFetch); } catch {}
// ================= INTERNAL STATE =================
const state = {
baseline: null,
reported: false,
firstDetectedAt: null
};
// ================= HELPERS =================
function safeToString(fn) {
try { return fnToString.call(fn); }
catch { return '[unavailable]'; }
}
function capture(path, obj, prop) {
const fn = prop ? obj[prop] : obj;
return {
path,
ref: fn,
name: fn?.name,
length: fn?.length,
toString: safeToString(fn),
descriptor: prop ? Object.getOwnPropertyDescriptor(obj, prop) : null,
proto: Object.getPrototypeOf(fn)
};
}
function isNativeLike(str) {
return /\{\s*\[native code\]\s*\}/.test(str);
}
function scoreFetch(snapshot) {
let score = 0;
const reasons = [];
if (!isNativeLike(snapshot.toString)) {
score += 3;
reasons.push('non-native toString');
}
if (snapshot.name !== 'fetch') {
score += 2;
reasons.push('unexpected function name');
}
if (snapshot.length !== 1) {
score += 1;
reasons.push('unexpected arity');
}
if (snapshot.descriptor?.configurable) {
score += 1;
reasons.push('configurable=true');
}
if (snapshot.proto !== Function.prototype) {
score += 3;
reasons.push('prototype mismatch');
}
return { score, reasons };
}
function snapshotRuntime() {
const out = {};
if (CONFIG.monitorFetch && window.fetch) {
out.fetch = capture('window.fetch', window, 'fetch');
out.fetch.analysis = scoreFetch(out.fetch);
}
if (CONFIG.monitorXHR && window.XMLHttpRequest) {
out.xhr = {
open: capture('XHR.open', XMLHttpRequest.prototype, 'open'),
send: capture('XHR.send', XMLHttpRequest.prototype, 'send')
};
}
return out;
}
function changed(a, b) {
if (a.ref !== b.ref) return true;
if (a.toString !== b.toString) return true;
if (a.proto !== b.proto) return true;
if (a.descriptor && b.descriptor) {
if (
a.descriptor.writable !== b.descriptor.writable ||
a.descriptor.configurable !== b.descriptor.configurable
) {
return true;
}
}
return false;
}
// ================= REPORTING =================
function report(data) {
if (!CONFIG.reportEnabled) return;
if (state.reported) return;
if (Math.random() > CONFIG.reportSampleRate) return;
state.reported = true;
const payload = {
ts: Date.now(),
firstDetectedAt: state.firstDetectedAt,
page: location.href,
ua: navigator.userAgent,
lang: navigator.language,
platform: navigator.platform,
hardwareConcurrency: navigator.hardwareConcurrency,
deviceMemory: navigator.deviceMemory,
data
};
try {
const body = JSON.stringify(payload);
if (navigator.sendBeacon) {
navigator.sendBeacon(
CONFIG.reportUrl,
new Blob([body], { type: 'application/json' })
);
} else if (safeFetch) {
safeFetch(CONFIG.reportUrl, {
method: 'POST',
body,
keepalive: true,
headers: { 'Content-Type': 'application/json' }
}).catch(() => {});
}
} catch {}
}
// ================= MAIN =================
state.baseline = snapshotRuntime();
// freeze baseline so it can’t be tampered with later
try {
Object.freeze(state.baseline);
if (state.baseline.fetch) Object.freeze(state.baseline.fetch);
if (state.baseline.xhr) Object.freeze(state.baseline.xhr);
} catch {}
setInterval(() => {
const current = snapshotRuntime();
let suspicious = false;
const findings = {};
if (current.fetch && state.baseline.fetch) {
if (changed(current.fetch, state.baseline.fetch)) {
suspicious = true;
findings.fetchMutation = true;
}
if (current.fetch.analysis.score >= CONFIG.suspicionThreshold) {
suspicious = true;
findings.fetchAnalysis = current.fetch.analysis;
}
}
if (suspicious) {
if (!state.firstDetectedAt) {
state.firstDetectedAt = Date.now();
}
report({
baseline: state.baseline,
current,
findings
});
if (CONFIG.logEnabled) {
warn('Possible Ajax Tampering Detected', {
baseline: state.baseline,
current,
findings
});
}
}
}, CONFIG.checkIntervalMs);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment