Last active
December 18, 2025 17:49
-
-
Save kidGodzilla/ad41c5240117bacb15da89effde39f81 to your computer and use it in GitHub Desktop.
Detect tampering with fetch/XMLHttpRequest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (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