Když na sdílený odkaz narazí crawler

Aktualizováno: · Autor: Radim Sekera

  1. 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:

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í

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

  1. Dejte host s tokeny pod robots.txt, ne jen pod noindex.
  2. Počítejte s tím, že každý odkaz stáhne preview bot a poštovní skener dřív než příjemce.
  3. Nechte stav měnící výměnu na POST spouštěném skriptem, nikdy na holém GETu.
  4. Hashujte tokeny at rest a porovnávejte je v konstantním čase.
  5. 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.
  6. Vybírejte vědomě mezi single-use a bezpečným pro skenery — replay a předčasné vypršení jdou proti sobě.
  7. 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.