sandbox

Jedan inline skript na samom vrhu <head> preuzima svaki browser API — mrežu, skladište, izvršavanje koda, senzore, otisak. Ništa ne prolazi dok politika ne kaže, sve što je strana pokušala ostaje zapisano, i sve odlazi u jedan jedini beacon pred zatvaranje strane.

1 · inicijalna skripta — neminimizovana, ide prva

Ovo je tačan izvor onoga što build ušije u stranicu. Nijedna linija nije skraćena. Mora biti prvi skript u dokumentu — sve što se izvrši pre njega, izvršilo se van kontrole.

src/tracker.js — izvor · …
…

Radi tri stvari i ništa više: zapleni mrežu i drži pozive u redu, hvata greške i konzolu od nulte milisekunde, povuče motor asinhrono. Sve ostalo — politika, otisak, vitals, otprema — stiže posle i ne blokira crtanje.

2 · kako se zove ovakav skript

inline bootstrap
opšti naziv — kod koji podigne sistem pre svega ostalog
head snippet
po mestu: prvi u <head>, blokira parser namerno
loader / tag
u analitici — ono što se nalepi na tuđu stranu
shim
po ulozi: stoji između pozivaoca i pravog API-ja
lockdown
postupak zamrzavanja globala da se ne mogu vratiti

U ovom projektu ga zovemo snippet, a ono što povlači motor. Snippet je jedini deo koji vlasnik strane vidi i jedini koji je na kritičnom putu. Motor sme da zakasni jer snippet drži red.

3 · kako se sprečava da ga neko poništi

Napad je jednostavan: skripta koja stigne posle nas napiše window.fetch = mojFetch i zaplena je gotova. Odgovor je da omotač ne postavljamo dodelom nego nepromenljivim i nepodesivim svojstvom.

Object.defineProperty(window, 'fetch', {
  value: shim,
  writable: false,      // w.fetch = x  →  TypeError u strict, tiho ne uspeva u sloppy
  configurable: false,  // delete w.fetch  →  ne uspeva; redefinisanje  →  TypeError
});
napadishod
window.fetch = mojFetchTypeError omotač ostaje
delete window.fetchne uspeva omotač ostaje
Object.defineProperty(window,'fetch',…)TypeError
iframe.contentWindow.fetchpresretnuto dete istog origina se zapleni pre predaje
document.createElement('script').src = …presretnuto setter na HTMLScriptElement.src
appendChild(<script>)presretnuto Node.appendChild gleda tagName
setTimeout('kod', 0)ide kroz eval kapiju

Isti postupak nosi i sam registar: window.__lpT je nepodesiv, pa se ne može zameniti lažnim. A S.verify() u svakom trenutku uporedi svaki globalni sa našim omotačem i vrati listu izmenjenih — pokreće se automatski pri app.ready.

Šta ovo ne rešava. Skripta koja stigne pre snippet-a je van dometa — zato snippet mora biti prvi u dokumentu. Za to postoji drugi sloj: Content-Security-Policy sa nonce-om, koji odlučuje na nivou browsera koji skript uopšte sme da se izvrši.

4 · jedan beacon, na kraju

Mehanizam je preuzet iz originala — api/s/b.js sa ltp.ge deploy-a, 2 039 b. Observeri se kače odmah, telo se sklapa tek pred zatvaranje, i šalje se tačno jednom.

// observeri odmah, buffered:true — hvataju i ono što se desilo pre njih
obs('largest-contentful-paint', …)   obs('layout-shift', …)   obs('event', …)

// tačno jedan odlazak; visibilitychange šalje samo ako je strana sakrivena
var sent = 0;
function send(force) {
  if (sent || (!force && document.visibilityState !== 'hidden')) return;
  sent = 1;
  navigator.sendBeacon(url, telo);
}
addEventListener('visibilitychange', function () { send(false); });
addEventListener('pagehide',         function () { send(true);  });

Telo nosi ekran, dpr, zonu, jezike, jezgra, memoriju, dodir, vezu, sklonosti (tema, reduced-motion, pointer), mrežni tajming (DNS, TCP, TLS, TTFB, DCL, load, transferSize), vitals (LCP, FCP, CLS, INP) — i, što original nije imao, presek sandboxa: šta je strana pozvala i šta joj je odbijeno. Samogejtovanje je na kolačiću, kao u originalu: već profilisana sesija ne šalje ponovo.

5 · šta se zaplenjuje

mreža
fetch · XHR · WebSocket · EventSource · sendBeacon · RTCPeerConnection · serviceWorker.register
izvršavanje
eval · Function · document.write · writeln · Worker · SharedWorker · setTimeout/setInterval sa stringom · script.src · appendChild(<script>)
skladište
localStorage · sessionStorage · cookie · indexedDB · caches · BroadcastChannel · storage.estimate
dozvole
geolocation (×2) · getUserMedia · getDisplayMedia · clipboard (read/write) · notifications · permissions · bluetooth · usb · serial · hid · wakeLock · share · credentials
otisak
canvas.toDataURL · toBlob · getImageData · WebGL.getParameter · AudioContext · getBattery · speechSynthesis.getVoices
navigacija
window.open · history.pushState · replaceState
bekstvo
iframe.contentWindow — dete istog origina se zapleni
posmatra se
console.* · onerror · unhandledrejection

Grupe se biraju pri build-u: --caps net,exec,store,perm,fp,nav,frame. Strana koja ne policira otisak ne nosi taj kod.

6 · ko sme da šalje, a ko da čita

Dva sasvim različita zahteva, pa dva mehanizma. Ključ u stranici je javan po prirodi — svako ga vidi. Token za čitanje je tajna i mora ostati tajna.

slanje · POST /a/p

Ne oslanja se na tajnost nego na poreklo:

ključ
mora postojati u registru SBX_SITES
Origin
mora biti na listi tog ključa (ili Referer)
brzina
brojač po ključu i minuti, u KV
slučajodgovor
ključ + poreklo204
tuđe poreklo403
nepoznat ključ403
bez ključa401
preko praga429
čitanje · GET /a/list, /a/rec

Bearer token iz SBX_TOKEN, poređen u konstantnom vremenu — dužina i sadržaj se ne odaju kroz tajming.

let diff = A.length ^ B.length; for (let i = 0; i < n; i++) diff |= (A.charCodeAt(i)||0) ^ (B.charCodeAt(i)||0); return diff === 0;

Nadzor traži token pri otvaranju i čuva ga u sessionStorage — nestaje sa karticom. Ako SBX_TOKEN nije podešen, čitanje je zatvoreno (503), ne otvoreno.

7 · report režim — politika bez lomljenja

Isti build, dva ponašanja. Otvori /test?mode=report i uporedi sa /test. Ovako se politika uvodi na živu stranu: prvo gledaš šta bi bilo odbijeno, pa stežeš.

enforcereport
rani poziv na zabranjeni origin sandbox: fetch nije dozvoljen Failed to fetch — pravi mrežni pokušaj
zahtev napusti browsernijedanda, i padne na DNS-u
zapisano u beaconfetch [enforce]fetch [report]
evalTypeErrorprolazi, ali se beleži

8 · politika i build

politika
{ "mode": "enforce", "allowByDefault": true, "capabilities": { "eval": false }, "network": { "allow": ["self", "*.ltp.ge"], "deny": ["https://evil.example"] }, "collector": "/a/p" }

report režim ne lomi ništa — samo beleži šta bi bilo odbijeno. Tako politika ulazi na živu stranu bez rizika.

build
node build.js # produkcija node build.js --full # snippet hvata i console i XHR node build.js --report # motor nosi presek za tablu node build.js --caps net,store # samo te grupe node build.js --no-log # beacon bez dnevnika

Build ušije politiku i adresu motora u snippet, spoji fragmente u jedan IIFE, minifikuje i izmeri.