Když na sdílený odkaz narazí crawler
- září 2026 nahlásil sken briefgate.dev v Bing Webmaster Tools dvě stránky
s chybějícím H1. Obě byly demo klientské portály na
p.briefgate.dev, každá s magic tokenem v query stringu (p.briefgate.dev/<slug>?t=…). Bing se k nim dostal přes redirect z veřejného demo odkazu na marketingovém webu — portál skutečného klienta nikde crawlovatelně odkázaný nebyl, ale ten demo, záměrně, odkázaný je.
Nic neuniklo: intaky za těmi URL jsou demo data, ověřeno proti databázi. Nikoho to ani nezamklo, protože token není single-use — proč, viz níž. Ale portálová URL nemá co dělat v crawl logu vyhledávače vůbec, a oprava stojí za sepsání, protože stejný problém má každý nástroj, který lidem posílá odkaz s přístupem uvnitř.
Čtyři druhy automatického otvírače
Odkaz poslaný člověku neotevírá jen ten člověk:
- Vyhledávací crawlery (Googlebot, Bingbot, nástroje na sken webu). Čím
dál víc renderují JavaScript, ale většinou ne při prvním průchodu — sken
nejdřív udělá rychlý synchronní
GETna syrové HTML, a jen část stránek se zařadí do pozdějšího, renderovaného průchodu. Samotné první stažení už zapíše URL i s tokenem do sebe. - Chat a link-preview fetchery — Slack, iMessage, WhatsApp, Discord,
Teams. Prostý
GETpostaví náhledovou kartu ve chvíli, kdy je odkaz vložený, ať už na něj někdo klikne, nebo ne. JavaScript neběží. - Bezpečnostní skenery pošty — Microsoft Defender Safe Links, Proofpoint,
Mimecast. Přepisují odkazy v příchozí poště a stahují originál v okamžiku
doručení, někdy znovu při kliknutí — klasický zabiják single-use tokenů,
protože
GETskeneru dokáže utratit odkaz dřív, než příjemce otevře e-mail. - Firemní web proxy a brány — odchozí filtry, které stáhnou URL jako součást skenování provozu, stejný tvar jako poštovní skener, jiný spouštěč.
Jen vyhledávací crawlery smysluplně spouští JavaScript, a ne při každém
požadavku. Zbylé tři jsou jen GET — žádný skript neběží. Jakákoli ochrana
závislá na běhu JavaScriptu neudělá nic pro tři ze čtyř.
„Je to neveřejné" není ochrana
Odkaz, na který nikdo neodkázal, působí soukromě. Není: preview bot ho stáhne
ve chvíli, kdy je vložen do kanálu, poštovní skener stáhne každý odkaz v
každé zprávě bez ohledu na to, kdo ji vidí, a stránka, která načítá vlastní
obrázky nebo analytiku, posílá aktuální URL jako Referer na tyhle
požadavky. Kdekoli se stažení zaloguje — v historii crawleru, v přístupovém
logu proxy, v dashboardu webmaster tools — fakt, že odkaz nikdy nebyl
odkázaný z indexované stránky, mu nezabrání být jednou stažen, a jednou
stačí, aby to zůstalo zapsané navždy. „Neveřejné" popisuje, jak odkaz najde
člověk; žádný ze čtyř otvíračů výše odkazy nenachází takhle.
Co se skutečně stalo, mechanicky
Portálová URL je <host>/<slug>?t=<token>. Stránka je vykreslovaná na
klientovi (Nuxt): počáteční HTML je skoro prázdná schránka a vlastní
JavaScript stránky přečte query parametr t při připojení, vymění ho za
session a pak zavolá history.replaceState(), aby token vytáhl zpátky z
adresního řádku. Bingovo rychlé první stažení tenhle skript nikdy nespustí —
čte syrové, skoro prázdné HTML, a proto sken nahlásil chybějící H1 místo
vykreslené stránky.
Ani samotná výměna není něco, co spustí GET. GET /portal/:slug vyžaduje
existující session cookie; tudy se token nevyměňuje. Výměna se děje na POST /portal/:slug/redeem, volaném vlastním skriptem stránky, nikdy prohlížečem,
který jen následuje odkaz. Otvírač, který umí jen GET — běžný případ pro tři
ze čtyř typů — nedokáže utratit token, založit session ani spustit webhook
client.viewed, ať už URL stáhne kolikrát chce.
I tam, kde se token vymění, se nespotřebuje: redeemMagicToken ho porovná
s uloženým hashem v konstantním čase a přijme ho, dokud nevyprší podle
vlastního rozvrhu (portal_link_ttl_days, výchozí 30 dní, počítáno od
posledního odeslaného e-mailu k tomu sběru podkladů), ne při prvním použití.
Přesně to zabránilo tomu, aby tenhle incident někoho zamkl — záměrný
kompromis, ne náhoda, která tentokrát pomohla.
Proč samotné noindex nestačilo
Tyhle stránky už měly meta tag noindex, a nestačilo to, protože crawler
musí stránku stáhnout, aby ho vůbec přečetl — stažení, query string včetně,
už proběhlo a zalogovalo se ve chvíli, kdy se crawler dozví, že nemá indexovat
to, co právě přečetl. Oprava, která řeší samotné stažení, je robots.txt na
portálovém hostu:
User-agent: *
Allow: /app/login
Disallow: /app.briefgate.dev a p.briefgate.dev jedou ze stejného buildu, takže jeden
soubor pokrývá oba. Dashboard je za loginem, takže indexovat stojí za to jen
přihlašovací stránku; všechno ostatní, portály především, je zakázané.
Allow: /app/login vyhraje nad Disallow: / podle nejdelší shody, takže
login zůstává crawlovatelný, zatímco každá cesta s tokenem zůstává nedotčená.
Poslušný crawler teď už nikdy nevydá GET, který by URL zalogoval — silnější
než žádat ho, ať neindexuje to, co už stáhl. robots.txt váže jen crawlery,
které ho respektují, a proto je to jedna vrstva, ne celá odpověď.
Konkrétní opatření
- Zakázat crawlování hostu s tokeny v
robots.txt— zastaví samotné stažení, ne jen záznam v indexu. noindexjako obrana do hloubky, užitečné proti crawlerům, kterérobots.txtignorují, k ničemu proti samotnému stažení.- Hashovat tokeny at rest. Portálové tokeny, stejně jako API klíče, jsou 256 bitů náhodnosti uložené jako SHA-256 digest; plaintext existuje jen v odkazu a krátce v paměti během výměny.
- Porovnávat v konstantním čase, se stejnými náklady na neznámý slug jako na známý, aby časování nešlo použít k uhodnutí tokenu nebo výčtu slugů.
- Prodlužovat platnost odkazu při každé upomínce, mezi odesláními ji
ohraničit. Sběr podkladů má jeden stálý odkaz, takže není co rotovat —
ale každá upomínka posune
portal_link_ttl_daysod svého odeslání dál, a když odesílání ustane, ustane i prodlužování. Starší kopie v poště nebo v logu crawleru zůstává živá jen tak dlouho, dokud se sběr podkladů ještě upomíná, a přestane fungovat, jakmile poslední odeslání vypadne z okna. Rozjetá session je samostatná cookie a je to bez vlivu na ni tak jako tak. - Držet session krátkou a omezenou — samostatná
httpOnlycookie omezená na cesty portálu, platná 14 dní, vázaná na jednu intake. - Vyměňovat přes
POST, nikdyGET— nic, co jen stáhne URL, nemůže nic utratit. - Rate-limitovat endpoint pro výměnu, podle IP i podle slugu.
- Vytáhnout token z adresního řádku hned po použití, aby ho nic, co
stránka dál načte, neposlalo dál v hlavičce
Refererani nenechalo v historii prohlížeče.
Proč token není single-use, záměrně
Spotřebovat token při prvním čtení vypadá jako zjevná oprava, ale vytváří
horší selhání: GET poštovního skeneru — otvírač, který nejspíš stáhne odkaz
dřív než člověk — by odkaz utratil dřív, než příjemce otevře schránku, a ten
by pak viděl „tenhle odkaz vypršel" bez vlastního přičinění. Token
BriefGate zůstává platný, dokud nevyprší podle vlastního rozvrhu — vypršení,
které další upomínka jen posune dál, ne že by nahradila samotný token.
Bezpečnost stojí na tom, že token je neuhodnutelný,
hashovaný at rest a rate-limitovaný na cestě výměny, ne na single use — stejná
vlastnost, která dělá systém odolný proti předčasnému stažení skenerem, je ta,
která tady udělala stažení crawlerem neškodné.
Když posíláte lidem odkazy s přístupem
- Dejte host s tokeny pod
robots.txt, ne jen podnoindex. - Počítejte s tím, že každý odkaz stáhne preview bot a poštovní skener dřív než příjemce.
- Nechte stav měnící výměnu na
POSTspouštěném skriptem, nikdy na holémGETu. - Hashujte tokeny at rest a porovnávejte je v konstantním čase.
- Ohraničte platnost odkazu a prodlužujte ji jen vlastním odesíláním, místo abyste spoléhali, že jeden odkaz zůstane platný navždy.
- Vybírejte vědomě mezi single-use a bezpečným pro skenery — replay a předčasné vypršení jdou proti sobě.
- Vytáhněte token z adresního řádku, jakmile je použitý.
Viz Bezpečnost pro to, jak BriefGate zachází s daty klientů, Upomínky pro to, jak fungují reminders a platnost odkazu do portálu od začátku do konce, a Portál pro to, co klient vidí na druhém konci odkazu.