notACMS 1.2.0 — co wyszło z audytu
php,symfony,statyczna-strona,open-source,bezpieczenstwo,ai1.2.0 zaczęło się jako porządki, przerodziło w pełny audyt i skończyło jako najbardziej znaczące wydanie notACMS do tej pory. Około 170 poprawek w rdzeniu, obu drzewach szablonów, dokumentacji i JS — z kilkoma celowymi zmianami niekompatybilnymi po drodze.
Kilka miesięcy temu pisałem o uruchomieniu audytu AI na notACMS i znalezieniu 79 bugów. 1.2.0 to kontynuacja: kolejne pełne przejście, tym razem z projektem w lepszym stanie i bogatszym AGENTS.md prowadzącym przegląd. Znalazło się sporo oczywistych rzeczy i kilka nieoczywistych. Oto te warte opisania.
Bug w nginx który po cichu psował formularze
Najbardziej frustrujące znalezisko: formularz kontaktowy który działał perfekcyjnie w każdym środowisku poza produkcją w Dockerze.
Konfiguracja nginx używała lokalizacji z regexem do routowania API kontaktowego:
location ~ ^/[a-z]{2}/api/contact$ {
Tylko że nie działała. W składni nginx {2} w pewnych kontekstach jest traktowane dosłownie — kwantyfikator był dopasowywany jako ciąg znaków, nie jako "dokładnie dwa znaki". Regex nigdy nie pasował, więc POST /pl/api/contact nigdy nie trafiał do PHP. Każda niedomyślna lokalizacja dostawała ciche 404 przy wysłaniu formularza.
Działało w DDEV (który ma własną konfigurację nginx) i lokalnie (gdzie PHP obsługuje routing inaczej). Docker produkcyjny, korzystający z commitowanego nginx.conf.template, był zepsuty od samego początku. Poprawka: jeden znak, zaescapowanie nawiasów klamrowych.
locale-redirect: pomocność szkodliwa
Skrypt locale-redirect.js czyta preferencje językowe przeglądarki i przekierowuje nowych użytkowników na ich lokalizację. Sensowny pomysł, ale logika miała dziurę.
Kiedy ktoś trafia na /pl/ pierwszy raz bez ciasteczka lang — powiedzmy, klikając w link — skrypt czytał navigator.language, znajdował en-US, ustawiał ciasteczko na en i przekierowywał go z dala od strony, na którą celowo wszedł.
Poprawka jest oczywista z perspektywy czasu: jeśli aktualny URL jest już na niedomyślnej lokalizacji, sam URL jest preferencją. Ustaw ciasteczko i stop. Bez przekierowania. Obejmuje to też środowiska HTTP — flaga Secure po cichu traciła ciasteczko w non-HTTPS (domyślna konfiguracja DDEV), uniemożliwiając działanie całego mechanizmu.
Dorzuciłem też zabezpieczenie przed scenariuszem którego wcześniej nie przewidziałem: przestarzałe ciasteczko z lokalizacją która zniknęła z _site.yaml. Bez niego usunięcie języka ze strony wpędzałoby każdego odwiedzającego ze starym ciasteczkiem w nieskończoną pętlę przekierowań na 404.
Excerpty wyszukiwarki i podwójne escapowanie
Przegląd bezpieczeństwa z 1.1.2 znalazł XSS w wyszukiwarce: excerpty z pagefind były wstrzykiwane do innerHTML bez escapowania. Naprawione przez owinięcie w esc().
Tyle że excerpty pagefind zawierają tagi <mark> — tak pagefind zaznacza dopasowania. Poprawka zepsuła wyróżnianie: <mark>term</mark> zamieniło się w dosłowny tekst <mark>term</mark>. Właściwa odpowiedź: treść pagefind to autorski statyczny HTML, nie dane wejściowe użytkownika. Tagi <mark> są wstrzykiwane przez silnik wyszukiwania na etapie budowania, nie przez odwiedzających. Usuń esc() z excerpta, zostaw wszędzie indziej. Niewidoczne dopóki nie zauważysz że wyróżnienia nie wyróżniają.
Bezpieczeństwo i utwardzenie
ImageMagick działa teraz przez Symfony\Process z tablicami argumentów zamiast ciągów shell — exec() zniknął. Turnstile weryfikuje hostname w odpowiedzi siteverify względem skonfigurowanego base_url, więc tokeny wygenerowane na domenie testowej nie mogą być odtworzone w produkcji. Output JSON-LD jest hex-escapowany — </script> w tytule posta nie może już wyłamać się ze skryptu. Nagłówki bezpieczeństwa nginx (X-Frame-Options, CSP itd.) nie docierały do odpowiedzi /assets/ i /media/ — add_header na poziomie lokalizacji wyciszał dziedziczone nagłówki serwera.
/llms.txt
Mały dodatek: trasa /llms.txt dołączona do statycznego builda, wylistowująca najnowsze wpisy w formacie czytelnym maszynowo — per lokalizacja, konfigurowalne przez llms_limit w _site.yaml, nadpisywalne per-motyw przez przestrzeń nazw Twig @base. Statyczne strony nie są tradycyjnie łatwe do nawigowania dla modeli językowych — to niskokosztowy sposób na dostarczenie kontekstu komukolwiek (lub czemukolwiek) kto to czyta.
Breaking changes
Kilka zmian wymagających jednolinijkowej migracji w local/src/:
ContentItem::directoryKey()zwraca teraz pełną ścieżkę treści (pages/aboutnieabout), naprawiając ciche kolizje URL między katalogami o tej samej nazwie w różnych sekcjachgetTree()przeniesione doContentTreeProviderInterface— zContentServiceInterfacestructured_data().blogPosting()przyjmuje nazwaną mapę (było 13 pozycyjnych argumentów)- Klucz kontekstu
lang_switch_urlusunięty — użyjlang_switch docs/customization/old-template/usunięty z repo — pobierz z taga v1.1.x jeśli nadal potrzebujesz
Pełny przewodnik migracji: UPGRADE-1.2.md.
Linki
Pełny changelog: CHANGELOG.md.
Repozytorium: GitHub / holas1337/notACMS — Apache 2.0.