HTML używa tych samych znaków do zapisu treści i struktury. Znak mniejszości może być częścią równania albo początkiem taga. Ampersand może być widocznym znakiem albo otwierać character reference. Encje HTML pozwalają wyrazić literalny znak w sposób, którego parser nie pomyli z markup. Są narzędziem gramatyki, nie szyfrowaniem i nie ukrywaniem danych.

Markup i tekst podróżują tym samym kanałem

Przeglądarka odczytuje tagi, atrybuty, komentarze i text z jednego dokumentu. W text context < może rozpocząć element, a & reference. Zapis &lt; i &amp; tworzy w DOM literalne znaki.

Named entity, na przykład &copy;, bywa czytelna, a numeric reference wskazuje Unicode code point. W UTF-8 większość znaków można zapisać bezpośrednio.

Escaping zależy od miejsca docelowego

Tekst między tagami i wartość wewnątrz atrybutu mają inne reguły. Quote jest szczególnie ważny w quoted attribute. JavaScript, CSS i URL posiadają własne składnie.

Jedna funkcja „escape everything” nie rozumie wszystkich przejść. Template powinien znać context, a developer unikać wstawiania untrusted values do inline script i event handler.

Encoding i sanitization rozwiązują inne problemy

Encoding pokazuje wpisane <strong> jako tekst. Sanitizer jest potrzebny, gdy użytkownik celowo może dodać ograniczony markup. Parser usuwa niebezpieczne tags, attributes i URL schemes.

Regex ani replace nie jest sanitizerem HTML. Browser error recovery i nesting tworzą zbyt wiele wariantów interpretacji.

Double encoding ujawnia dwóch właścicieli

Jeżeli zakodowany ampersand zostanie escaped ponownie, &lt; staje się &amp;lt;. Użytkownik widzi składnię entity zamiast znaku. Backend i template prawdopodobnie oba wykonały prezentacyjną operację.

Plain text powinien pozostać semantycznym Unicode w storage i zostać escaped dopiero przy renderowaniu. Encoded output nie jest dobrym ogólnym modelem danych.

Reference nie zmienia znaczenia Unicode

Bezpośredni znak UTF-8, decimal reference, hex reference i named entity mogą dać ten sam znak w DOM. Source strings są różne, wynik parsera równoważny.

Unicode normalization to osobny temat. Wizualnie identyczne teksty mogą mieć inne sekwencje code points, a entity tego nie naprawia.

Source, DOM i visible text to trzy poziomy

View Source pokazuje odpowiedź serwera. Inspector pokazuje DOM po parsing, error recovery i zmianach JavaScript. Skopiowany tekst pokazuje to, co odbiera użytkownik.

Diagnoza encji wymaga wskazania poziomu. Sekwencja widoczna w source może już nie istnieć jako tekst w DOM albo zostać dodana później przez client code.

Browser jest tolerancyjny, filtr nie może zgadywać

Malformed HTML często zostaje naprawiony do działającego DOM. Prosty filtr może przewidzieć inną strukturę i przepuścić payload. Mature encoder oraz sanitizer modeluje rzeczywiste zachowanie parsera.

Test security powinien sprawdzać końcowy DOM i wykonanie, nie tylko obecność substringu w response.

Template powinien domyślnie wybierać bezpieczną ścieżkę

Zwykła interpolacja jest escaped automatycznie. Raw HTML helper wymaga jawnego, sanitized typu i review. Dzięki temu bezpieczeństwo nie zależy od pamięci każdej osoby.

Jeżeli untrusted text został wcześniej połączony z markup, auto-escaping nie potrafi odzyskać pierwotnej granicy.

UTF-8 ogranicza niepotrzebne encje

Historycznie references pomagały przy ograniczonych charsetach. Nowoczesny dokument może przechowywać polskie i inne znaki bezpośrednio. Source pozostaje czytelny dla redakcji.

Warto escape'ować znaki składniowe i używać entity tam, gdzie jasno komunikuje intencję, zamiast zamieniać każdy non-ASCII.

Plain text i sanitized HTML powinny mieć inne typy

Gdy oba są zwykłym stringiem, developer musi zgadywać, czy raw rendering jest dozwolony. Nazwa body_html, schema i wrapper type pokazują pochodzenie.

API również deklaruje rodzaj pola. Obecność angle brackets nie jest wiarygodnym detektorem, bo zwykły tekst może je zawierać.

Odpowiedzialność za escaping powinna być widoczna w API

Backend zwracający plain text nie powinien decydować, jak klient HTML zapisze ten tekst w dokumencie. Ten sam string może trafić do aplikacji natywnej, CSV lub powiadomienia. Każdy renderer posiada własny kontekst i wykonuje właściwą transformację.

Jeżeli serwer zwraca gotowe sanitized HTML, pole powinno być nazwane i udokumentowane osobno. Consumer nie może zakładać, że każdy string z tego endpointu jest bezpieczny do raw insertion.

Source portability wymaga jednej konwencji

Zespół może używać bezpośredniego UTF-8 dla zwykłych liter oraz entities dla znaków syntaktycznych lub niewidocznych. Ważniejsza od maksymalnej liczby encji jest spójność między CMS, formatterem i importerem.

Automatyczna zamiana wszystkich znaków non-ASCII wydłuża diff i utrudnia korektę tekstu bez poprawy bezpieczeństwa. Konwencja powinna służyć czytelności oraz stabilnemu round trip.

Parser może zmienić strukturę bez zmiany source przez autora

Nieprawidłowe zagnieżdżenie powoduje, że browser zamyka i przenosi elementy według algorytmu recovery. Skrypt oraz CSS pracują już na naprawionym DOM. Security review musi więc rozumieć wynik parsera, a nie tylko intencję autora stringa.

Test fixtures z malformed markup są szczególnie ważne dla sanitizerów i treści importowanych ze starszych edytorów.

Walidator nie zastępuje testu w browserze

Validator wykrywa wiele błędów source, ale nie wszystkie interakcje z DOM i skryptami. Legacy oraz third-party content należy sprawdzać w rzeczywistym rendererze.

Finalny DOM jest strukturą używaną przez CSS, JavaScript i accessibility APIs. Jego znaczenie jest ważniejsze niż pozornie poprawny string.

Encje są narzędziem składni

Pozwalają zachować granicę między content i markup. Praktyczna reguła brzmi: przechowuj semantyczny tekst, escape dla dokładnego output context i sanitizuj tylko świadomie dozwolone HTML.

Ta odpowiedzialność usuwa zarówno widoczne artefakty double encoding, jak i istotną klasę injection vulnerabilities.