Začátkem září 2018 jsme s borcema z Optimicsu naprogramovali proof-of-concept JavaScriptového trackeru Fakin, který se snaží dokázat, že pokud chceme, nelze trackování (v tomto případě do Google Analytics) zablokovat nástroji jako Ghostery, Privacy badger, uBlock origin, Firefox – Anonymní okno s ochranou proti sledování a další.
Tento koncept jsem představil na nekonferenci MeasureCamp v Brně, kde si tato přednáška odnesla nejvíce hlasů a proto si zaslouží podrobnější popis, než jen pár slajdů z Impressu. Také bych chtěl uvést, že si moc vážím zájmu o toto téma a děkuji za Google Daydream, které jsem si tímto získal. Své místo mají v Optimicsu.
Proč jsme vůbec programovali Fakina?
Na internetu lze dohledat mnoho statistik ohledně blokování reklamy a analytických scriptů v internetu a jejich závěry jsou různé. Vstupuje do toho rozsah analyzovaných technologií, demografie, uživatelská zařízení a další proměnné. Některé statistiky hovoří o 11% blokovaných scriptů, některé až o 60%. Souhrnnější studie se potkávají na cca 25% blokovaných reklam a analytiky v internetu.
Zdroje:
- https://pagefair.com/blog/2017/adblockreport/
- https://www.statista.com/topics/3201/ad-blocking/
- https://www.businessinsider.com/pagefair-2017-ad-blocking-report-2017-1
- https://help.getadblock.com/support/solutions/articles/6000087874-adblock-is-using-lots-of-memory
- https://marketingland.com/survey-shows-us-ad-blocking-usage-40-percent-laptops-15-percent-mobile-216324
Kde jsou mý data?
Fajn, takže vy se snažíte dělat závěry nad daty z Google Analytics (Adobe SiteCatalyst, Piwik, whatever software), ale chybí vám 25% dat. V tento moment už lze doporučit ke zpřesnění dat použití magické koule (zde k prodeji za 999,-), nebo použití současně nejpopulárnějšího řešení, a to je smířit se s tím.
Někteří provozovatelé webu vytáhli do boje a používají nějakou z forem vydírání, jako např. detectadblock.com, kdy uživateli s AdBlockem zobrazí otravný overlay zamezující konzumaci obsahu. Ten, dokud nepovolí na webu reklamy, má utrum. Svým způsobem toto řešení chápu, ale o eleganci se nedá vůbec mluvit. Rozhodně to není řešení, jelikož takovéto AdBlock Wally lidi vysloveně serou.
Máš problém
Ať se na situaci koukneme z jakékoliv strany, pokud jste konzumentem dat, máte problém. A ten jednoznačně nemá klesající tendence (viz. https://neilpatel.com/blog/intelligent-tracking-prevention/ | https://www.techrepublic.com/article/how-to-enable-tracking-protection-in-firefox-quantum/ | https://techcrunch.com/2018/04/12/firefox-updates-its-ios-web-browser-to-turn-tracking-protection-on-by-default/ | https://brave.com/).
Proto jsme napsali Fakina, abychom dokázali, že blokování není cesta. Chceme dokázat, že cesta je umírněné a transparentní využívání dat. Takové, se kterým uživatelé nebudou mít problém a sbíraná data jim mohou být kuprospěchu.
Nechceme podporovat „špehovací histerii“, kdy jsou všichni podělaní z trackování, takže si blokují kde jaké statistické scripty (včetně naprosto neškodných měření), ale pak na sebe vše nabonzují na Facebooku, mají cracklá Wokna s hromadou spywaru a čumí na péčko v „Anonymním okně„.
Nám, analytikům, statistikům, data-minerům a marketérům to kazí práci.
Proof-of-concept
Na celé doméně https://dataretard.cz/ sídlí jen Proof-of-concept trakaře do Google Analytics Fakin. Ten web nedělá nic jiného. Pokud kliknete na tlačítko Debugga buttona, v konsoli si s vámi Fakin začně povídat. Vzhledem k tomu, že se o vás nesbírají žádná osobní data, ani IP adresa a nic se nesdílí s 3rd party doménami, chybí tam břečka o cookies a paranoia z GDPR. Web bez JavaScriptu nejede.
Základní koncepty Fakina
URLs – adresy
Každá méně, či více populární služba (API pro sběr dat) má nějakou URL adresu, kam posílá data. K jejímu zablokování stačí trapný regulární výraz v BlackListu a máte po ptákách.
Namátkou:
- google-analytics.com/collect – Googles API for data collection
- googletagmanager.com – CDN for container delivery
- omntrdc.net – Adobe SiteCatalyst
- assets.adobedtm.com – Adobe Dynamic Tag Manager
- track.adform.net – trakaří API Adformu
- facebook.com/tr/ – trakaří API Faceboku
- doubleclick.net – Google Doubleclick Ad network
- hit.gemius.pl – trakaří API Gémia
- … a kilometr dalších
Takže první věc, co potřebujeme pořešit je skrytí těchto adres. K tomu nám pomáhej CNAME, nebo PROXY.
Vlastní domény, CNAME záznamy, PROXY servery
Jsme chytří a svůj tracking přesměrujeme z adresy https://google-analytics.com/collect na adresu https://tr.dataretard.cz/ (zde tr figuruje jako zkratka pro tracking či trakař).
Chvilku nám to asi bude šlapat, ale jen do doby, než přijde nějaký šulín z AdBlocku a přidá nás na blacklist. Ne proto, že bychom někoho špehovali, nýbrž jen proto, že si dovolujeme sbírat data o užívání svojí služby. Chápete? Jsou to fašisti. Pro ilustraci doporučuji přečíst diskusi pod tímto článkem.
Adresa tr.dataretard.cz či dataretard.cz/t/ má dvě slabiny.
- šulín z AdBlocku
- generický název tr (ad, analytics…) a může být blokován mnohem obecnějšími pravidly.
Takže druhá věc, co potřebujeme řešit je skrytí těchto PROXY. K tomu nám pomáhej router.
Fakin Router
Tady už to začíná být skutečně zajímavé. Víte co mají tyto tři adresy (a dalších pár miliard) společné?
- https://dataretard.cz/tunak-s-horcici1/
- https://dataretard.cz/mrouskajicise-velryba-s-okurkou-polozenou-vertikalne-na-mrazaku/
- https://dataretard.cz/splouchajici-pastynak-nad-parnim-hrncem/
Pojďme si třeba postavit generátor náhodných řetězců ze slovníků českých spisovných výrazů (1.8 MB).
Soubory
- analytics.js
- appmeasurement.js
- satellite.*\.js
- event.js
- piwik.js
- gtm.js
- ga.js
- hotjar.*\.js
- a kilometr dalších?
Asi si dovedete představit, že zablokovat je nebude problém, opět pomocí nějakého trapného blacklistu. Takže je musíme přejmenovat. Ale co když přijde šulín z AdBlocku a zablokuje nám náš script? Je to jasné, musíme randomizovat.
Ano, jsou to adresy scriptů obsahující trakař Fakina. A ano, je jich konečně mnoho.
Další alternativa je mrsknout trakař třeba k souboru načítající jQuery nebo jakýkoliv jiný nebohý script. K zablokování trakaře byste museli killnout třeba celý web. Nic moc řešení. Pokud nejste číňan. Tam killnutí celého webu chápu.
Trakař sám
Paráda, máme bezpečnou cestu ze serveru ke klientovi, máme bezpečnou cestu z klienta na server, ale co samotné běhové prostředí?
Potřebujeme dosáhnout několika úrovní skrytí.
- Randomizovat JavaScript na serveru ze šablon.
- měnit jména globálních proměnných
- přidávat zbytečné konstrukce
- přeskupovat bloky kódu
- Obfuskovat. Ideálně pro každý request mít jiný seed obfuskátoru.
- Skrývat se v dynamicky měněných jmenných prostorech
Ukázka rané verze fakina (bez seedů v obfuskátoru, bez randomizace).
(function(_0xa886x2, _0xa886x3) { _0x9eb5[1]; var _0xa886x4 = { behavior: { debug: false, d: navigat0r[_0x9eb5[2]] }, ana: { cok: _0x9eb5[3] } }; var _0xa886x5 = _0x9eb5[4]; function _0xa886x6(_0xa886x7) { var _0xa886x8 = console; var _0xa886x9 = _0xa886x7 || false; (this[_0x9eb5[5]] = function(_0xa886xa) { _0xa886x9 ? _0xa886x8[_0x9eb5[5]](_0xa886xa) : _0x9eb5[6] }), (this[_0x9eb5[7]] = function(_0xa886xa) { _0xa886x9 ? _0xa886x8[_0x9eb5[7]](_0xa886xa) : _0x9eb5[6] }), (this[_0x9eb5[8]] = function(_0xa886xa) { _0xa886x9 ? _0xa886x8[_0x9eb5[8]](_0xa886xa) : _0x9eb5[6] }), (this[_0x9eb5[9]] = function(_0xa886xa) { _0xa886x9 ? _0xa886x8[_0x9eb5[9]](_0xa886xa) : _0x9eb5[6] }), (this[_0x9eb5[10]] = function(_0xa886xa) { _0xa886x9 ? _0xa886x8[_0x9eb5[10]](_0xa886xa) : _0x9eb5[6] }), (this[_0x9eb5[11]] = function() { _0xa886x9 ? _0xa886x8[_0x9eb5[11]]() : _0x9eb5[6] }), (this[_0x9eb5[12]] = function(_0xa886xa) { _0xa886x9 ? _0xa886x8[_0x9eb5[12]](_0xa886xa) : _0x9eb5[6] }) } function _0xa886xb(_0xa886xc) { return _0xa886x2[_0x9eb5[13]](unescape(encodeURIComponent(_0xa886xc))) } function _0xa886xd(_0xa886xc) { return decodeURIComponent(escape(_0xa886x2[_0x9eb5[14]](_0xa886xc))) } function _0xa886xe(_0xa886xf, _0xa886x10) { return (_0xa886x10 = RegExp(_0x9eb5[17] + encodeURIComponent(_0xa886xf) + _0x9eb5[18])[_0x9eb5[16]](_0xa886x3[_0x9eb5[15]])) ? _0xa886x10[2] : null } function _0xa886x11(_0xa886xa) { var _0xa886x12 = _0xa886xa[_0x9eb5[20]](/[^a-zA-Z0-9]/g, _0x9eb5[19]); return _0xa886x12 } function _0xa886x13(_0xa886xf) { var _0xa886x14 = _0xa886xe(_0xa886x11(_0xa886xb(_0xa886xf))); if (_0xa886x14) { return _0xa886xd(_0xa886x14) } else { return null } } function _0xa886x15(_0xa886x16, _0xa886x17, _0xa886x18) { var _0xa886x19 = _0x9eb5[6]; if (_0xa886x18) { var _0xa886x1a = new Date(); _0xa886x1a[_0x9eb5[22]](_0xa886x1a[_0x9eb5[21]]() + _0xa886x18 * 24 * 60 * 60 * 1000); _0xa886x19 = _0x9eb5[23] + _0xa886x1a[_0x9eb5[24]]() }; _0xa886x3[_0x9eb5[15]] = _0xa886x16 + _0x9eb5[25] + (_0xa886x17 || _0x9eb5[6]) + _0xa886x19 + _0x9eb5[26] } function _0xa886x1b(_0xa886x16, _0xa886x17, _0xa886x18) { var _0xa886x1c = _0xa886x11(_0xa886xb(_0xa886x16)); var _0xa886x1d = _0xa886xb(_0xa886x17); _0xa886x4a[_0x9eb5[5]](_0x9eb5[27] + _0xa886x16 + _0x9eb5[28] + _0xa886x1c + _0x9eb5[29] + _0xa886x17 + _0x9eb5[28] + _0xa886x1d + _0x9eb5[30]); _0xa886x15(_0xa886x1c, _0xa886x1d, _0xa886x18) } function _0xa886x1e() { var _0xa886x1f = _0x9eb5[6]; var _0xa886x20 = _0x9eb5[31]; var _0xa886x21 = _0xa886x27(23, 64); for (var _0xa886x22 = 0; _0xa886x22 < _0xa886x21; _0xa886x22++) { _0xa886x1f += _0xa886x20[_0x9eb5[35]](Math[_0x9eb5[34]](Math[_0x9eb5[32]]() * _0xa886x20[_0x9eb5[33]])) }; return _0xa886x1f } function _0xa886x23(_0xa886xa, _0xa886x12) { var _0xa886x8 = {}; for (var _0xa886x24 in _0xa886xa) { _0xa886x8[_0xa886x24] = _0xa886xa[_0xa886x24] }; for (var _0xa886x24 in _0xa886x12) { _0xa886x8[_0xa886x24] = _0xa886x12[_0xa886x24] }; return _0xa886x8 } function _0xa886x25(_0xa886xa) { var _0xa886x12 = []; for (var _0xa886x8 in _0xa886xa) { _0xa886xa[_0x9eb5[36]](_0xa886x8) && _0xa886x12[_0x9eb5[37]](encodeURIComponent(_0xa886x8) + _0x9eb5[25] + encodeURIComponent(_0xa886xa[_0xa886x8])) }; return _0xa886x12[_0x9eb5[39]](_0x9eb5[38]) } function _0xa886x26(_0xa886xa) { var _0xa886x12 = new Image; _0xa886x12[_0x9eb5[40]] = _0xa886xa } function _0xa886x27(_0xa886xa, _0xa886x12) { return Math[_0x9eb5[34]](Math[_0x9eb5[32]]() * (_0xa886x12 - _0xa886xa + 1)) + _0xa886xa } function _0xa886x28() { function _0xa886x24(_0xa886xa) { var _0xa886x12; if (_0xa886xa) { _0xa886x12 = _0xa886xa[_0x9eb5[41]] + _0x9eb5[42] + _0xa886xa[_0x9eb5[43]] } else { if (!_0xa886xa && _0xa886x2[_0x9eb5[44]]) { try { var _0xa886x8 = _0xa886x2[_0x9eb5[44]][_0x9eb5[48]][_0x9eb5[47]][_0x9eb5[46]]()[_0x9eb5[45]](); _0xa886x12 = _0xa886x8[_0x9eb5[41]] + _0x9eb5[42] + _0xa886x8[_0x9eb5[43]] } catch (_0xa886xa) {} } }; return _0xa886x12 } var _0xa886xa = _0xa886x2, _0xa886x12 = _0xa886xa[_0x9eb5[49]], _0xa886x8 = _0xa886xa[_0x9eb5[50]], _0xa886x29 = _0xa886x8 && _0xa886x8[_0x9eb5[51]] ? _0xa886x8[_0x9eb5[51]] : _0xa886x8 && _0xa886x8[_0x9eb5[52]] ? _0xa886x8[_0x9eb5[52]] : _0x9eb5[6]; return (_0xa886x29 + _0x9eb5[53] + _0xa886x29 + _0x9eb5[53] + _0xa886x24(_0xa886x12) + _0x9eb5[53] + (_0xa886x12 ? _0xa886x12[_0x9eb5[54]] : _0x9eb5[6])) } function _0xa886x2a() {} function _0xa886x2b(_0xa886x2c) { var _0xa886x2d = {}; for (var _0xa886x2e in _0xa886x2c) { if (_0xa886x2c[_0x9eb5[36]](_0xa886x2e)) { _0xa886x2d[_0xa886xb(_0xa886x2e)] = _0xa886xb(_0xa886x2c[_0xa886x2e]) } }; return _0xa886x2d } function _0xa886x2f(_0xa886xc) { _0xa886xc = _0xa886xc[_0x9eb5[57]](_0x9eb5[56])[_0x9eb5[55]](); var _0xa886x2d = _0xa886xc[_0x9eb5[57]](_0x9eb5[38]); var _0xa886x30 = {}; for (var _0xa886x22 = 0; _0xa886x2d[_0x9eb5[33]] > _0xa886x22; _0xa886x22++) { var _0xa886x31 = _0xa886x2d[_0xa886x22][_0x9eb5[57]](_0x9eb5[25]); _0xa886x30[_0xa886xd(decodeURIComponent(_0xa886x31[0]))] = _0xa886xd(decodeURIComponent(_0xa886x31[1])) }; return _0xa886x30 } function _0xa886x32(_0xa886x33, _0xa886x34) { if (typeof _0xa886x34[_0x9eb5[58]] != _0x9eb5[59]) { _0xa886x4a[_0x9eb5[9]](_0x9eb5[60]); _0xa886x4a[_0x9eb5[7]](_0xa886x34); _0xa886x51(_0xa886x34[_0x9eb5[58]], _0xa886x34) } else { _0xa886x4a[_0x9eb5[10]](_0x9eb5[61]); _0xa886x4a[_0x9eb5[7]](_0xa886x34) }; if (typeof(_0xa886x34[_0x9eb5[62]]) !== _0x9eb5[59] && _0xa886x34[_0x9eb5[62]] === _0x9eb5[63] && typeof(_0xa886x34[_0x9eb5[64]]) !== _0x9eb5[59] && _0xa886x34[_0x9eb5[64]] === _0x9eb5[65]) { _0xa886x1b(_0x9eb5[66], _0x9eb5[67]) }; _0xa886x4a[_0x9eb5[11]]() }(function() { var _0xa886x35 = /\[object (Boolean|Number|String|Function|Array|Date|RegExp)\]/; function _0xa886x36(_0xa886xa) { return null == _0xa886xa ? String(_0xa886xa) : (_0xa886xa = _0xa886x35[_0x9eb5[16]](Object[_0x9eb5[70]][_0x9eb5[69]][_0x9eb5[68]](Object(_0xa886xa)))) ? _0xa886xa[1][_0x9eb5[71]]() : _0x9eb5[72] } function _0xa886xf(_0xa886xa, _0xa886x12) { return Object[_0x9eb5[70]][_0x9eb5[36]][_0x9eb5[68]](Object(_0xa886xa), _0xa886x12) } function _0xa886x37(_0xa886xa) { if (!_0xa886xa || _0x9eb5[72] != _0xa886x36(_0xa886xa) || _0xa886xa[_0x9eb5[73]] || _0xa886xa == _0xa886xa[_0x9eb5[74]]) { return !1 }; try { if (_0xa886xa[_0x9eb5[75]] && !_0xa886xf(_0xa886xa, _0x9eb5[75]) && !_0xa886xf(_0xa886xa[_0x9eb5[75]][_0x9eb5[70]], _0x9eb5[76])) { return !1 } } catch (b) { return !1 }; for (var _0xa886x8 in _0xa886xa) {; }; return void(0) === _0xa886x8 || _0xa886xf(_0xa886xa, _0xa886x8) } function _0xa886x38(_0xa886xa, _0xa886x12, _0xa886x8) { this[_0x9eb5[77]] = _0xa886xa; this[_0x9eb5[78]] = _0xa886x12 || function() {}; this[_0x9eb5[79]] = !1; this[_0x9eb5[80]] = {}; this[_0x9eb5[81]] = []; this[_0x9eb5[82]] = _0xa886x3d(this); _0xa886x10(this, _0xa886xa, !_0xa886x8); var _0xa886x24 = _0xa886xa[_0x9eb5[37]], _0xa886x29 = this; _0xa886xa[_0x9eb5[37]] = function() { var _0xa886x12 = [][_0x9eb5[83]][_0x9eb5[68]](arguments, 0), _0xa886x8 = _0xa886x24[_0x9eb5[84]](_0xa886xa, _0xa886x12); _0xa886x10(_0xa886x29, _0xa886x12); return _0xa886x8 } } _0xa886x2[_0x9eb5[85]] = _0xa886x38; _0xa886x38[_0x9eb5[70]][_0x9eb5[86]] = function() { this[_0x9eb5[80]] = {} }; _0xa886x38[_0x9eb5[70]][_0x9eb5[87]] = function(_0xa886xa) { var _0xa886x12 = this[_0x9eb5[80]]; _0xa886xa = _0xa886xa[_0x9eb5[57]](_0x9eb5[88]); for (var _0xa886x8 = 0; _0xa886x8 < _0xa886xa[_0x9eb5[33]]; _0xa886x8++) { if (void(0) === _0xa886x12[_0xa886xa[_0xa886x8]]) { return }; _0xa886x12 = _0xa886x12[_0xa886xa[_0xa886x8]] }; return _0xa886x12 }; _0xa886x38[_0x9eb5[70]][_0x9eb5[89]] = function() { this[_0x9eb5[77]][_0x9eb5[90]](0, this[_0x9eb5[77]][_0x9eb5[33]]); this[_0x9eb5[77]][0] = {}; _0xa886x3f(this[_0x9eb5[80]], this[_0x9eb5[77]][0]) }; function _0xa886x10(_0xa886xa, _0xa886x12, _0xa886x8) { for (_0xa886xa[_0x9eb5[81]][_0x9eb5[37]][_0x9eb5[84]](_0xa886xa[_0x9eb5[81]], _0xa886x12); !1 === _0xa886xa[_0x9eb5[79]] && 0 < _0xa886xa[_0x9eb5[81]][_0x9eb5[33]];) { _0xa886x12 = _0xa886xa[_0x9eb5[81]][_0x9eb5[91]](); if (_0x9eb5[92] == _0xa886x36(_0xa886x12)) { _0xa886xa: { var _0xa886x24 = _0xa886x12, _0xa886x29 = _0xa886xa[_0x9eb5[80]]; if (_0x9eb5[93] == _0xa886x36(_0xa886x24[0])) { for (var _0xa886x39 = _0xa886x24[0][_0x9eb5[57]](_0x9eb5[88]), _0xa886x3a = _0xa886x39[_0x9eb5[55]](), _0xa886x24 = _0xa886x24[_0x9eb5[83]](1), _0xa886x3b = 0; _0xa886x3b < _0xa886x39[_0x9eb5[33]]; _0xa886x3b++) { if (void(0) === _0xa886x29[_0xa886x39[_0xa886x3b]]) { break _0xa886xa }; _0xa886x29 = _0xa886x29[_0xa886x39[_0xa886x3b]] }; try { _0xa886x29[_0xa886x3a][_0x9eb5[84]](_0xa886x29, _0xa886x24) } catch (v) {} } } } else { if (_0x9eb5[94] == typeof _0xa886x12) { try { _0xa886x12[_0x9eb5[68]](_0xa886xa[_0x9eb5[82]]) } catch (w) {} } else { if (_0xa886x37(_0xa886x12)) { for (var _0xa886x3c in _0xa886x12) { _0xa886x3f(_0xa886x3e(_0xa886x3c, _0xa886x12[_0xa886x3c]), _0xa886xa[_0x9eb5[80]]) } } else { continue } } }; _0xa886x8 || ((_0xa886xa[_0x9eb5[79]] = !0), _0xa886xa[_0x9eb5[78]](_0xa886xa[_0x9eb5[80]], _0xa886x12), (_0xa886xa[_0x9eb5[79]] = !1)) } } function _0xa886x3d(_0xa886xa) { return { set: function(_0xa886x12, _0xa886x8) { _0xa886x3f(_0xa886x3e(_0xa886x12, _0xa886x8), _0xa886xa[_0x9eb5[80]]) }, get: function(_0xa886x12) { return _0xa886xa[_0x9eb5[87]](_0xa886x12) } } } function _0xa886x3e(_0xa886xa, _0xa886x12) { for (var _0xa886x8 = {}, _0xa886x24 = _0xa886x8, _0xa886x29 = _0xa886xa[_0x9eb5[57]](_0x9eb5[88]), _0xa886x39 = 0; _0xa886x39 < _0xa886x29[_0x9eb5[33]] - 1; _0xa886x39++) { _0xa886x24 = _0xa886x24[_0xa886x29[_0xa886x39]] = {} }; _0xa886x24[_0xa886x29[_0xa886x29[_0x9eb5[33]] - 1]] = _0xa886x12; return _0xa886x8 } function _0xa886x3f(_0xa886xa, _0xa886x12) { for (var _0xa886x8 in _0xa886xa) { if (_0xa886xf(_0xa886xa, _0xa886x8)) { var _0xa886x24 = _0xa886xa[_0xa886x8]; _0x9eb5[92] == _0xa886x36(_0xa886x24) ? (_0x9eb5[92] == _0xa886x36(_0xa886x12[_0xa886x8]) || (_0xa886x12[_0xa886x8] = []), _0xa886x3f(_0xa886x24, _0xa886x12[_0xa886x8])) : _0xa886x37(_0xa886x24) ? (_0xa886x37(_0xa886x12[_0xa886x8]) || (_0xa886x12[_0xa886x8] = {}), _0xa886x3f(_0xa886x24, _0xa886x12[_0xa886x8])) : (_0xa886x12[_0xa886x8] = _0xa886x24) } } } })(); if (true) {}; function _0xa886x40() { var _0xa886x41 = []; var _0xa886x42 = !1; function _0xa886x43() { if (_0xa886x42) { return !0 }; if (_0xa886x41[_0x9eb5[33]] > 0) { _0xa886x4d[_0x9eb5[37]](_0xa886x41[_0x9eb5[91]]()); _0xa886x42 = !0; setTimeout(function() { _0xa886x42 = !1 }, 90); if (_0xa886x41[_0x9eb5[33]] > 0) { return !0 }; return !1 }; return !1 } function _0xa886x44() { if (_0xa886x43()) { setTimeout(function() { _0xa886x44() }, 100) } } this[_0x9eb5[37]] = function(_0xa886xa) { _0xa886x41[_0x9eb5[37]](_0xa886xa); _0xa886x44() } } function _0xa886x45() { var _0xa886x46 = _0xa886x27(1000000, 9999999) + _0x9eb5[88] + _0xa886x27(1000000, 9999999); _0xa886x4a[_0x9eb5[5]](_0x9eb5[95] + _0xa886x46); var _0xa886x47 = _0xa886x4[_0x9eb5[97]][_0x9eb5[96]]; _0xa886x1b(_0xa886x47, _0xa886x46); return _0xa886x46 } function _0xa886x48() { var _0xa886x46 = _0xa886x13(_0xa886x4[_0x9eb5[97]][_0x9eb5[96]]); _0xa886x4a[_0x9eb5[5]](_0x9eb5[98] + _0xa886x46); if (_0xa886x46 != null) { return _0xa886x46 } else { return _0xa886x45() } } var _0xa886x49 = (_0xa886x4[_0x9eb5[100]][_0x9eb5[99]] || _0xa886x13(_0x9eb5[66]) === _0x9eb5[67]); var _0xa886x4a = new _0xa886x6(_0xa886x49); _0xa886x4a[_0x9eb5[9]](_0x9eb5[101]); var _0xa886x4b = _0xa886x2[_0xa886x4[_0x9eb5[100]][_0x9eb5[79]]] || []; _0xa886x2[_0x9eb5[102]] = { push: function(_0xa886xa) { Array[_0x9eb5[70]][_0x9eb5[37]][_0x9eb5[84]](_0xa886x2[_0xa886x4[_0x9eb5[100]][_0x9eb5[79]]], [_0xa886xa]) } }; var _0xa886x4c = []; for (key in _0xa886x4b) { _0xa886x4c[_0x9eb5[37]](_0xa886x4b[key]) }; var _0xa886x41 = new _0xa886x40(); var _0xa886x4b = (_0xa886x2[_0xa886x4[_0x9eb5[100]][_0x9eb5[79]]] = { push: function(_0xa886xa) { _0xa886x41[_0x9eb5[37]](_0xa886xa) } }); for (key in _0xa886x4c) { _0xa886x41[_0x9eb5[37]](_0xa886x4c[key]) }; var _0xa886x4d = _0xa886x4d || []; var _0xa886x4e = new DataLayerHelper(_0xa886x4d, _0xa886x32, true); var _0xa886x4f = { "\x76": _0x9eb5[103], "\x74\x69\x64": _0x9eb5[104], "\x6F\x72\x64": _0xa886x27(10000000000, 999999999999), "\x63\x69\x64": _0xa886x48(), "\x64\x68": _0xa886x3[_0x9eb5[106]][_0x9eb5[105]], "\x64\x70": _0xa886x3[_0x9eb5[106]][_0x9eb5[107]], "\x63\x64\x31": _0xa886x2[_0x9eb5[106]][_0x9eb5[108]], "\x64\x74": _0xa886x3[_0x9eb5[109]], "\x63\x6D\x31": _0xa886x28() }; function _0xa886x50() { return _0xa886x4f } var _0xa886x51 = function(_0xa886x52, _0xa886x53) { _0xa886x4a[_0x9eb5[9]](_0x9eb5[110]); if (_0xa886x52 == _0x9eb5[111]) { _0xa886x52 = _0x9eb5[112] } else { _0xa886x52 = _0x9eb5[58] }; var _0xa886x53 = _0xa886x53 || {}; var _0xa886x54 = _0xa886x23(_0xa886x23(_0xa886x50(), _0xa886x53), { t: _0xa886x52 }); _0xa886x4a[_0x9eb5[5]](_0x9eb5[113]); _0xa886x4a[_0x9eb5[8]](_0xa886x54); var _0xa886x55 = _0xa886x2b(_0xa886x54); var _0xa886x56 = _0xa886x5 + _0x9eb5[114] + _0xa886x1e() + _0x9eb5[56] + _0xa886x25(_0xa886x55); _0xa886x26(_0xa886x56); _0xa886x4a[_0x9eb5[5]](_0x9eb5[115] + _0xa886x56); _0xa886x4a[_0x9eb5[5]](_0x9eb5[116]); _0xa886x4a[_0x9eb5[8]](_0xa886x55); _0xa886x4a[_0x9eb5[5]](_0x9eb5[117]); _0xa886x4a[_0x9eb5[8]](_0xa886x2f(_0xa886x25(_0xa886x55))); _0xa886x4a[_0x9eb5[11]]() }; _0xa886x4a[_0x9eb5[11]](); return !0 })(window, document);
DataLayer
Aby se neřeklo, že je Fakin nepoužitelná magořina, má plnou podporu dataLayeru z Google Tag Manageru.
Akorát když použijete dataLayer, tak jste v čoudu, jelikož stačí, aby nějaký trapný adblocker udělal:
if(typeof(window.dataLayer)!=="undefined") { window.dataLayer = { push : function(){ return !0; } }; }
Pápá šmudlinko. Nemáme dataLayer.
Ok, jestli jste to dočetli až sem, pak vás asi napadne změnit název dataLayeru, což by na nějakou dobu mohlo zabrat, ale dataLayer se celkem dobře hledá. Jednak se na něj budou odkazovat event listenery, jednak bude mít metody push, set, get, reset a další znaky, podle kterého jej poznáme.
Takže co s tím?
- na serveru opět musíme randomizovat názvy globálních proměnných
- musíme randomizovat namespace, ve kterém se bude dataLayer nacházet a tuto šablonu pak použít i u event listenerů v aplikaci, kterou chceme trackovat.
- dále můžeme dataLayer skrýt za wrapper a nenechat jej číst z venčí
- můžeme cyklicky ověřovat, zda dataLayer existuje a pokud ne, můžeme se jej pokusit znovu vygenerovat z konstruktoru
To by snad mohlo zabrat, ne?
Cookies, localStorage
Leckdo zná cookies názvů:
- _ga
- _gat
- _gid
- _utma, _utmb,_utmc,_utmt,_utmz
- _dc_gtm
- svid
- AMCV_
- mbox
Ty lze přeci taky snadno detekovat a ničit, takže i zde musíme mlžit. Vzhledem k jejich persistenci jsou zranitelnější, ale pro začátek bude stačit, když je budeme kryptovat.
Ano, zde je použito triviální base64 a heverzmazdy je _ga cookie. Zde by se určitě dalo více šifrovat a randomizovat názvy. Zároveň by se dalo při každém znovunačtení stránky vzít staré cookies a přegenerovat je do nových jmen.
Payload
Posledním slabým místem je samotný payload do Google Analytics. Ten je definován podle vzoru jménem Measurement Protocol a tudíž je dopředu známý.
Ve Fakinovi jsme obsah zprávy pouze zakódovali v base64, opět se tedy jedná v tomto konceptu o slabý článek (jelikož base64 není žádná šifra, jen forma encodování), ale pro ilustraci stačí. V ostřejším řešení bychom použili silnější šifrování, či přidali nějakou server-side generovanou mapovací funkci, která by hodnoty napohled zrandomizovala. Zde by se celkově veliká část konstrukce payloadu dala řešit až na PROXY.
To je vše
Právě jste viděli Fakina, doufám, že se vám líbí. Budu rád za komentáře ke zlepšení, či jen když se pochlubíte, jakého Fakina jste si naprogramovali sami s touto báječnou kuchařkou.
A pokud byste měli zájem o jeho produkční nasazení, neváhejte mě kontaktovat a to buď přímo, nebo přes Optimics, nejlepší agenturu na digitální marketing.
Nemožné na počkání, zázraky do tří dnů.
S láskou, Váš, Pravý Český Tuňák
Skvele ve zkratce rozebrane techniky AdBlockeru a jejich obejiti. Velmi prinosny prispevek jak pro jedince neznale v teto tematice, tak pro zkusene analytiky. Za me cenny a strucny, avsak informacemi nadupany clanek! Diky!
F.
Za mně super potenciál – spíš tedy pro PRO analytiky. Z hlediska GDPR bullshitu je to vlastně taky ok, když ty lidiuž tak jako tak odsouhlasej cookie lišty a další souhlasy, který tam už stejně musí bejt.
Díky za návod. NoScript na tyhle sráče pomáhá
Mrzí mě, že Vám unikla hlavní myšlenka tohoto článku. Pokud si vás budu chtít odtrackovat, stále mohu použít server side metody. A v článku je zmíněno, že web bez JavaScriptu nejede, tedy vaše řešení je pouze Denial of Service. A to není moc uspokojivé řešení. Spíš doufám, že se rozvinou řešení jako Solid.
Tenhle článek a postup v něm popsaný je gigantický etický přešlap (i když se autor přiblble snaží naznačovat že není).
„Chceme dokázat, že cesta je umírněné a transparentní využívání dat. Takové, se kterým uživatelé nebudou mít problém a sbíraná data jim mohou být kuprospěchu.“ – Uživatelé s tím nebudou mít problém protože jim lžete. Až zjistí že jim lžete, budou s tím mít ještě větší problém.
„Ne proto, že bychom někoho špehovali, nýbrž jen proto, že si dovolujeme sbírat data o užívání svojí služby.“ – tím že data přeposíláte Googlu data nesbíráte vy ale Google, a to je nejčistší definice špehování.
Well done, hrdost na dobře vykonané dílo je na místě, podobně jako když se dítěti poprvé podaří urkást v obchodě pytlík bonbónů.
Děkuji Pavle za Váš pohled.
Jako přiblblý autor bych si jen dovolil poznamenat, že ono sdílení dat s Googlem má několik rovin. Jednak faktickou, tedy že se sdílejí jen telemetrická data o webu bez jednoznačné vazby na uživatele (což právě díky proxy dělá i zmíněný koncept Fakin), tak právní, kdy u služby Google Analytics 360 jsou data v právním vlastnictví přímo vás, jako entity platící za službu. Jinak se to chová u Free verze Google Analytics, kde data skutečně poskytujete Google, což ale není problém Google, ale provozovatele webu, který s takovýmto trade-off souhlasí.
A co se týče poznámky o tom, že uživateli lžeme, zajímalo by mě čím? Tím, že na základě pochopení webu je web optimalizován? Že jsou důležité informace snáze dostupné? Že lze odhalovat UX chyby? Že se služby přizpůsobuje uživateli a nenutí ho k jednomu předem vymyšlenému scénáři? že analytika je základ pro zpětnovazebné mechanismy?
Vidím to jinak a jsem hrdý na svůj pytlík bonbonů.