Dostępny na nowe rolePiotr Czerwiński

Blog · 9 września 2026 · 8 min czytania

Jak nagłówek instrukcji dla LLM trafił do moich publicznych danych strukturalnych

AI governance · LLM · dane strukturalne · bezpieczeństwo · context engineering

TL;DR: Nagłówek z metadanymi, który napisałem dla modelu ekstrakcji, był przechowywany w tym samym polu co oczyszczony tekst ogłoszenia. Krok publikacji dopisany później kopiował to pole do danych strukturalnych JSON-LD na publicznych stronach. Odwiedzający nigdy go nie widzieli, ale widziały go wyszukiwarki i każdy crawler czytający dane strukturalne, na dziesiątkach stron serwisu z ogłoszeniami, który prowadzę. Znalazłem to, otwierając źródło strony, którą crawler SEO oflagował z zupełnie innego powodu. Naprawa objęła usuwanie nagłówka na każdej publicznej granicy, rozwiązanie awaryjne publikujące krótkie podsumowanie, gdy usunięcie nie jest bezpieczne, oraz poprawkę danych wstecz (backfill). Reguła, która z tego wynikła: tekst dla modelu jest składany w chwili wywołania, kod budujący nagłówek i kod, który go usuwa, są w jednym module, a każda publiczna granica czytająca pole wewnętrzne dostaje mapper oraz test, który podstawia treść przypominającą prompt i sprawdza, że nie wychodzi ona na zewnątrz.

Jak nagłówek promptu trafił do publicznych danych strukturalnych?

Serwis z ogłoszeniami działa na małym pipeline. Pobiera ogłoszenie, czyści HTML i wysyła tekst do modelu językowego, który wyciąga z niego ustrukturyzowane pola. Żeby ekstrakcja była pewniejsza, pipeline dokleja na początku tekstu krótki nagłówek: fakty znane już ze źródła, takie jak lokalizacja i biura, oznaczone jako wiarygodne, żeby model przedkładał je nad domysły z treści. To zwykły context engineering: kształtowanie tego, co trafia do okna kontekstu modelu, żeby wynik był lepszy.

Problem tkwił w tym, gdzie ten nagłówek był przechowywany. Oczyszczony tekst razem z nagłówkiem lądował w bazie, bo ekstraktor przy ponownych przebiegach potrzebuje tego samego wejścia. Osobny krok publikacji, napisany później i w innym celu, brał publiczny opis każdego ogłoszenia dosłownie z tego pola. Widoczna strona renderowała krótkie podsumowanie, więc człowiek nie zauważyłby niczego złego. Blok JSON-LD, czyli dane strukturalne do odczytu maszynowego, z których wyszukiwarki budują rozszerzone wyniki, dostawał pełny opis, z nagłówkiem na początku.

W nagłówku nie było sekretów ani danych użytkowników. Mimo to był to tekst napisany dla modelu, opublikowany dla każdej wyszukiwarki i każdego crawlera, który czyta dane strukturalne. OWASP Top 10 dla aplikacji LLM wymienia wyciek promptu systemowego jako osobną kategorię, a to był mały, przyziemny przypadek właśnie tego. Model nie zrobił nic złego. Wyciek wziął się z instalacji wokół niego.

Jak znaleźć wyciek promptu na publicznej stronie?

Nie patrząc na stronę. Właśnie dlatego wyciek przetrwał. Znalazłem go przy okazji. Audyt SEO serwisu oflagował błędy danych strukturalnych w całym serwisie i poprawiłem je we wspólnym kodzie layoutu. Po ponownym crawlu trzy strony nadal miały ostrzeżenie o podwójnie escapowanych encjach HTML w opisie. Otworzyłem źródło jednej z nich, żeby zobaczyć faktyczny JSON-LD, a opis zaczynał się od mojego nagłówka dla ekstraktora.

Dalej chodziło o ustalenie skali. Przeskanowałem wszystkie ogłoszenia z sitemapy i znalazłem nagłówek w danych strukturalnych dziesiątek działających stron. Trzy oflagowane strony były tylko tymi, na których wyciek przy okazji zepsuł escapowanie HTML. Reszta to poprawne dane strukturalne, które akurat zawierały kontekst promptu, a na to żaden walidator nigdy nie zaprotestuje.

To niewygodna część. Narzędzia sprawdzają, czy dane strukturalne są poprawnie zbudowane, a nie czy ich treść była przeznaczona dla publiczności. Taki wyciek jest niewidoczny dla użytkowników, przechodzi walidację i wychodzi na jaw dopiero wtedy, gdy człowiek przeczyta źródło.

Naprawić dane, rendering czy przestać przechowywać nagłówek?

Miałem trzy opcje i każda osobno była niewystarczająca.

  • Tylko poprawić przechowywane dane. Czyści dzisiejsze wiersze. Kolejny przebieg pipeline zapisze nagłówek ponownie, a kolejna publikacja znowu go skopiuje. To leczenie objawów.
  • Tylko usuwać nagłówek przy renderowaniu. Od razu zabezpiecza publiczne strony bez dotykania danych. Ale brudne pole zostaje, a każdy przyszły konsument tego pola (feed RSS, API, drugi serwis czytający tę samą bazę, feed przygotowany dla crawlerów LLM) też musi pamiętać o usuwaniu. Poleganie na pamięci to dokładnie to, co do tej sytuacji doprowadziło.
  • W ogóle przestać przechowywać nagłówek. Składać go w chwili wywołania modelu i nigdy nie zapisywać obok tekstu do publikacji. To właściwy kształt na dłuższą metę. Oznaczał jednak zmianę po stronie ekstrakcji, która działa w starszym serwisie będącym akurat w trakcie migracji, a ekstraktor naprawdę potrzebuje nagłówka przy ponownych przebiegach.

Połączyłem dwie pierwsze opcje, a trzecią zostawiłem jako regułę dla całego nowego kodu. Przechowywane pole zostaje, jawnie traktowane jako wewnętrzne, z nietkniętym nagłówkiem, bo ekstraktor go potrzebuje. Każda publiczna granica go usuwa. Istniejące wiersze zostały poprawione wstecz, więc same dane są czyste. To daje obronę w głąb (defense in depth): jeśli nowa ścieżka publikacji zapomni o usuwaniu, dane pod spodem już są czyste, a jeśli pipeline znowu zapisze nagłówek w polu do publikacji, zatrzyma go granica.

Co trafiło na produkcję i czego nie wyłapała pierwsza poprawka

Pierwsza poprawka przeniosła kod budujący nagłówek i kod, który go usuwa, do jednego modułu. Krok publikacji i builder danych strukturalnych usuwają nagłówek na publicznej granicy, a test podaje na wejście realistyczny nagłówek i sprawdza, że na wyjściu nie ma tekstu przeznaczonego dla modelu:

// illustrative shape
it("never publishes model-facing context", () => {
  const stored = buildModelHeader({ location: "Remote" }) + "Real body";
  const out = toPublicStructuredData({ description: stored });
  expect(JSON.stringify(out)).not.toContain(MODEL_HEADER_MARKER);
});

Po tym deployu większość dotkniętych stron była czysta. Pozostałe nie i z tej części warto wyciągnąć naukę. Te ogłoszenia zostały przetworzone kilka miesięcy wcześniej, gdy nagłówek miał inny format dla każdego źródła. Część starszych wariantów była zapisana jako zwykły tekst, bez znaczników wskazujących, gdzie nagłówek się kończy, i zlana z treścią. Nie było bezpiecznego miejsca do cięcia. Każda reguła, jaką mógłbym napisać, albo zostawiłaby część nagłówka, albo zjadła część prawdziwego opisu.

Dlatego druga poprawka zmieniła politykę zamiast regexa. Kod usuwający nauczył się tych starszych formatów, które potrafił obsłużyć niezawodnie. Gdy nagłówek jest nadal wykrywany po usunięciu, dane strukturalne publikują krótkie podsumowanie zamiast pełnego opisu, a ścieżka publikacji rzuca wyjątek zamiast zapisywać. Nigdy nie zgaduj, nigdy nie publikuj. Nieco uboższy opis to niewielka cena za gwarancję, że tekst promptu nie wyjdzie na zewnątrz.

Poprawka danych wstecz przyszła na końcu. To skrypt, który domyślnie działa na sucho (dry run) i zapisuje dopiero z jawną flagą. Potrzebuje klucza produkcyjnego, który celowo trzymam poza lokalnym środowiskiem, więc uruchomiłem go sam. Sam dry run był osobną lekcją: nagłówek siedział w kilka razy większej liczbie zapisanych wierszy, niż sugerował skan działających stron, łącznie z ogłoszeniami, które nie były już publikowane. Skan publicznych stron mierzy dzisiejszą ekspozycję. Nie mówi nic o tym, co odsłoniłaby następna ponowna publikacja. Większość wierszy została oczyszczona na miejscu, część odbudowana z wewnętrznego tekstu źródłowego, kilka dostało podsumowanie, a mała grupa w jeszcze innym formacie wymagała drugiego przebiegu z jawnym cięciem. Trzeci dry run zgłosił zero wierszy i ten sam dry run służy teraz jako monitor: oczekiwany wynik to zero.

Jaka jest teraz reguła governance?

AI governance zwykle oznacza zasady tego, jak organizacja korzysta z modeli: jakie dane wchodzą, co wychodzi, kto to sprawdza. W skali jednej osoby prowadzącej produkt oznacza kilka reguł na tyle konkretnych, że da się je egzekwować testami. Tę regułę wpisałem do globalnych instrukcji, które moje agenty kodujące ładują w każdym projekcie, bo to agenty piszą większość kodu mojego pipeline i potrzebują tej reguły w chwili, gdy piszą nową ścieżkę publikacji:

  1. Składaj tekst dla modelu w chwili wywołania. Nie przechowuj go w polu, które cokolwiek publikuje. Jeśli musi zostać zapisany, pole jest jawnie wewnętrzne i nic publicznego nie czyta go bez mappera.
  2. Każda publiczna granica, która czyta pole wewnętrzne, ma jawny mapper i test. Test podstawia treść przypominającą prompt i sprawdza, że nie wychodzi ona na zewnątrz. Publiczne granice to strona, JSON-LD, meta tagi, RSS, sitemapa, obrazy Open Graph, API i każdy feed przygotowany dla LLM.
  3. Builder i kod usuwający są w jednym module. Starsze warianty, które pokonały pierwszą poprawkę, to dokładnie to, co się dzieje, gdy format i jego usuwanie ewoluują w różnych plikach.
  4. Po każdej zmianie pipeline otwórz źródło jednej nowej strony. Przeczytaj JSON-LD, meta tagi i feed, a nie tylko wyrenderowany widok. Tak znalazłem ten wyciek i żaden automat nie znalazłby go wcześniej.

U podstaw leży pochodzenie danych (data lineage): wiedza o tym, skąd pochodzi dany fragment tekstu i dla kogo został napisany. Nagłówek był nieszkodliwy w kontekście, dla którego powstał. Stał się wyciekiem, bo nazwa pola nic nie mówiła o jego odbiorcy, a późniejszy kod rozsądnie założył, że opis to po prostu opis.

Co powiedziałbym komuś, kto jutro uruchamia pipeline z LLM

  • Spisz swoje publiczne powierzchnie i pola, które każda z nich czyta. Jeśli którekolwiek z tych pól kiedykolwiek przeszło przez prompt, dana powierzchnia potrzebuje mappera i testu.
  • Oznacz odbiorcę w modelu danych. Pola z tekstem dla modelu nie powinno się dać pomylić z tekstem dla ludzi.
  • Wybieraj rozwiązanie awaryjne zamiast sprytnego cięcia. Gdy nie da się niezawodnie usunąć nagłówka, opublikuj coś bezpiecznego i krótszego, a ścieżka zapisu niech głośno zgłasza błąd.
  • Poprawkę danych wstecz zaczynaj od dry run i ufaj jego liczbom bardziej niż skanowi działających stron. Zapisane dane zwykle kryją więcej niż to, co jest akurat widoczne.
  • Czytaj źródło, nie stronę. Walidatory sprawdzają kształt. Intencję sprawdzi tylko człowiek albo celowany test.

Jedno ograniczenie zostaje. Reguła obejmuje granice, o których wiem. Drugi serwis, który czyta tę samą bazę, trafił na listę dalszych działań tego samego dnia, bo cały incydent wziął się z granicy, której nikt nie spisał.

Pytania, na które odpowiada ten wpis

Jak tekst promptu może wyciec do danych strukturalnych JSON-LD?
Dzieje się tak, gdy tekst napisany dla modelu, na przykład nagłówek z kontekstem, jest przechowywany w tym samym polu co treść do publikacji, a późniejszy krok publikacji kopiuje to pole na stronę. Widoczna strona może wyglądać dobrze, a blok JSON-LD i tak pokazuje tekst promptu wyszukiwarkom i crawlerom.
Jak zapobiec pojawianiu się promptów LLM na publicznych stronach?
Składaj tekst dla modelu w chwili wywołania modelu, zamiast przechowywać go obok treści do publikacji. Każda publiczna granica, która czyta pole wewnętrzne, powinna mieć jawny mapper oraz test, który podstawia treść przypominającą prompt i sprawdza, że nie wychodzi ona na zewnątrz. Kod budujący nagłówek promptu i kod, który go usuwa, trzymaj w jednym module, żeby ich formaty nie mogły się rozjechać.
Jak sprawdzić, czy na stronie nie ma wycieku tekstu promptu?
Otwórz źródło strony i przeczytaj JSON-LD, meta tagi i feedy, bo walidatory sprawdzają, czy dane strukturalne są poprawnie zbudowane, a nie czy ich treść miała być publiczna. Rób to na jednej nowej stronie po każdej zmianie w pipeline, który korzysta z LLM.