Ten blog jest zbudowany w innym stacku niż reszta moich projektów, celowo. Reszta portfolio to głównie Astro albo Django z Astro po froncie. Tutaj: Flask, PostgreSQL i panel administracyjny pisany od zera, bez gotowego CMS-a.
Dlaczego Flask, a nie coś gotowego
WordPress albo Ghost dałyby ten sam efekt widoczny na zewnątrz szybciej. Ale ten serwis miał inny cel niż samo publikowanie tekstów: pokazać, jak wygląda praca z frameworkiem, w którym nic nie dzieje się "magicznie", od modelu danych, przez sanityzację treści z edytora, po deploy.
Stack: Flask jako aplikacja, SQLAlchemy z Flask-Migrate do modelu i migracji, PostgreSQL jako baza produkcyjna (Neon, serverless), Cloudinary do obrazków, Quill jako edytor WYSIWYG w panelu administracyjnym.
Największy problem: bezpieczne przechowywanie treści z edytora
Edytor WYSIWYG zwraca HTML. Ten HTML trzeba gdzieś zapisać, a potem wyświetlić na stronie publicznej. Zapisanie go bez żadnej obróbki to prosta droga do XSS, każdy, kto ma dostęp do panelu (albo znajdzie sposób, żeby go zdobyć) mógłby wstrzyknąć skrypt wykonujący się w przeglądarce każdego czytelnika.
Rozwiązanie: biblioteka bleach z jawną whitelistą dozwolonych tagów i atrybutów. Żadnego <script>, żadnych atrybutów on*, żadnego javascript: w linkach. Osadzenia YouTube nie są wyjątkiem od tej reguły, są jej dowodem: edytor nigdy nie zapisuje gotowego <iframe>, tylko bezpieczny znacznik z samym ID filmu. Prawdziwa ramka jest generowana dopiero w momencie wyświetlania strony, nie wcześniej.
Druga warstwa: nagłówki bezpieczeństwa
Content Security Policy ustawiona na default-src 'self', bez unsafe-inline i bez unsafe-eval w script-src. To wymusiło konkretne decyzje: fonty Google musiały zostać ściągnięte i hostowane lokalnie, bo CSP bez dodanego font-src po prostu by je zablokowała. Każdy inline onclick czy onsubmit w szablonach musiał zniknąć, zamiast niego zwykłe listenery w plikach JS podpięte po stronie klienta.
Restrykcyjna CSP wymusza uczciwość: nie da się "na szybko" wkleić inline skryptu, trzeba znaleźć właściwe miejsce w kodzie.
Trzecia warstwa: wersjonowanie sanityzacji
Reguły sanityzacji zmieniają się w czasie, na przykład gdy dodaje się nowy dozwolony tag do edytora. Problem: co z treścią zapisaną PRZED tą zmianą? Każdy wpis ma pole sanitizer_version, a komenda flask resanitize przelicza starą treść nowymi regułami, bez potrzeby ręcznego przechodzenia po każdym wpisie z osobna.
Co bym zrobił inaczej
Gdybym zaczynał ten projekt od nowa, prawdopodobnie wprowadziłbym testy end-to-end na sanityzację od pierwszego dnia, nie po fakcie. Dwa błędy znalezione w trakcie pisania prawdziwej treści (znikające podpisy pod zdjęciami w galerii, gubiona litera "ł" w adresach URL) ujawniły się dopiero wtedy, gdy przez system przeszła treść bliższa rzeczywistej niż sztuczne dane testowe. Wniosek praktyczny: syntetyczne przypadki testowe nie zastąpią przepuszczenia realnej treści przez cały pipeline.