Implementowanie debugowania CSP i Zaufanych typów w Narzędziach deweloperskich w Chrome

Kateryna Prokopenko
Kateryna Prokopenko
Alfonso Castaño
Alfonso Castaño

Ten post na blogu dotyczy implementacji obsługi Narzędzi deweloperskich do debugowania problemów ze standardem Content Security Policy (CSP) za pomocą wprowadzonej niedawno karty Problemy.

Prace nad wdrożeniem zostały wykonane w ramach 2 staży: 1. Podczas pierwszego etapu utworzyliśmy ogólne ramy raportowania i zaprojektowaliśmy komunikaty o problemach dotyczące 3 naruszeń zasad CSP. 2. W drugim dodaliśmy problemy z typami zaufanymi oraz kilka specjalistycznych funkcji Narzędzi deweloperskich do debugowania typów zaufanych.

Czym jest standard Content Security Policy?

Content Security Policy (CSP) umożliwia ograniczenie niektórych zachowań w witrynie w celu zwiększenia bezpieczeństwa. Na przykład CSP można użyć do zablokowania skryptów wbudowanych lub eval, co zmniejsza powierzchnię ataku w przypadku ataków typu cross-site scripting (XSS). Szczegółowe wprowadzenie do CSP znajdziesz tutaj.

Szczególnie nową zasadą CSP jest zasada Trusted Types(TT), która umożliwia dynamiczną analizę, która może systematycznie zapobiegać dużej klasie ataków typu „wstrzykiwanie kodu” na strony internetowe. Aby to osiągnąć, TT obsługuje witrynę w zakresie nadzorowania kodu JavaScript, aby zezwalać na przypisywanie do ujść DOM, takich jak innerHTML, tylko określonych typów elementów.

Witryna może aktywować standard Content Security Policy, dodając określony nagłówek HTTP. Na przykład nagłówek content-security-policy: require-trusted-types-for 'script'; trusted-types default aktywuje zasady TT dla strony.

Każda zasada może działać w jednym z tych trybów:

  • tryb egzekwowania, w którym każde naruszenie zasad jest błędem;
  • tryb tylko raportowania – zgłasza komunikat o błędzie jako ostrzeżenie, ale nie powoduje błędu na stronie internetowej.

Problemy z wdrażaniem standardu Content Security Policy na karcie Problemy

Celem tych działań było ułatwienie debugowania problemów z CSP. Podczas rozpatrywania nowych problemów zespół Narzędzi deweloperskich postępuje mniej więcej według tego procesu:

  1. Definiowanie historyjek użytkownika. Zidentyfikuj zestaw historyjek użytkownika w interfejsie Narzędzi deweloperskich, które pokazują, jak programista stron internetowych powinien zbadać problem.
  2. Implementacja front-endu. Na podstawie historyjek użytkownika określ, które informacje są wymagane do zbadania problemu we front-endzie (np.powiązane żądanie, nazwa pliku cookie, wiersz w skrypcie lub pliku HTML itp.).
  3. Wykrywanie problemów. Zidentyfikuj miejsca w przeglądarce, w których można wykryć problem w Chrome, i skonfiguruj je tak, aby zgłaszały problem, podając odpowiednie informacje z kroku (2).
  4. Zapisz i wyświetl problemy. Przechowuj problemy w odpowiednim miejscu i udostępniaj je Narzędziom deweloperskim po ich otwarciu.
  5. Projektowanie tekstu dotyczącego problemów. Przygotuj tekst objaśniający, który pomoże programiście stron internetowych zrozumieć problem i go rozwiązać.

Krok 1. Definiowanie historyjek użytkownika dotyczących problemów z CSP

Zanim rozpoczęliśmy prace nad implementacją, utworzyliśmy dokument projektowy z historyjkami użytkowników, aby lepiej zrozumieć, co musimy zrobić. Na przykład zapisaliśmy taką historyjkę użytkownika:


Jako deweloper, który właśnie zdał sobie sprawę, że niektóre części mojej witryny są zablokowane, chcę: – dowiedzieć się, czy CSP jest przyczyną blokowania elementów iframe lub obrazów w mojej witrynie; – dowiedzieć się, która dyrektywa CSP powoduje blokowanie określonego zasobu; – dowiedzieć się, jak zmienić CSP mojej witryny, aby umożliwić wyświetlanie obecnie zablokowanych zasobów lub wykonywanie obecnie zablokowanego kodu JS.


Aby przeanalizować tę historyjkę użytkownika, utworzyliśmy kilka prostych przykładowych stron internetowych, które wykazywały naruszenia zasad CSP, którymi byliśmy zainteresowani, i przeanalizowaliśmy je, aby zapoznać się z tym procesem. Oto kilka przykładowych stron internetowych (otwórz wersję demonstracyjną z otwartą kartą Problemy):

Dzięki temu procesowi dowiedzieliśmy się, że lokalizacja źródła jest najważniejszą informacją do debugowania problemów z CSP. Przydatne okazało się też szybkie znajdowanie powiązanego elementu iframe i żądania w przypadku zablokowania zasobu. Przydatny może być też bezpośredni link do elementu HTML w panelu Elementy w Narzędziach deweloperskich.

Krok 2. Implementacja front-endu

Na podstawie tych informacji stworzyliśmy pierwszą wersję roboczą informacji, które chcieliśmy udostępnić w Narzędziach deweloperskich za pomocą protokołu Narzędzi deweloperskich w Chrome (CDP):

Poniżej znajduje się fragment pliku third_party/blink/public/devtools_protocol/browser_protocol.pdl.

 type ContentSecurityPolicyIssueDetails extends object
   properties
     # The url not included in allowed sources.
     optional string blockedURL
     # Specific directive that is violated, causing the CSP issue.
     string violatedDirective
     boolean isReportOnly
     ContentSecurityPolicyViolationType contentSecurityPolicyViolationType
     optional AffectedFrame frameAncestor
     optional SourceCodeLocation sourceCodeLocation
     optional DOM.BackendNodeId violatingNodeId

Powyższa definicja zasadniczo koduje strukturę danych JSON. Jest on napisany w prostym języku PDL (Protocol Data Language). PDL jest używany do 2 celów. Najpierw używamy PDL do wygenerowania definicji TypeScript, na których opiera się interfejs Narzędzi deweloperskich. Na przykład powyższa definicja PDL generuje ten interfejs TypeScript:

export interface ContentSecurityPolicyIssueDetails {
  /**
  * The url not included in allowed sources.
  */
  blockedURL?: string;
  /**
  * Specific directive that is violated, causing the CSP issue.
  */
  violatedDirective: string;
  isReportOnly: boolean;
  contentSecurityPolicyViolationType: ContentSecurityPolicyViolationType;
  frameAncestor?: AffectedFrame;
  sourceCodeLocation?: SourceCodeLocation;
  violatingNodeId?: DOM.BackendNodeId;
}

Po drugie, co jest prawdopodobnie ważniejsze, na podstawie definicji generujemy bibliotekę C++, która obsługuje generowanie i wysyłanie tych struktur danych z backendu Chromium w C++ do frontendu Narzędzi deweloperskich. Za pomocą tej biblioteki można utworzyć obiekt ContentSecurityPolicyIssueDetails, używając tego fragmentu kodu C++:

protocol::Audits::ContentSecurityPolicyIssueDetails::create()
  .setViolatedDirective(d->violated_directive)
  .setIsReportOnly(d->is_report_only)
  .setContentSecurityPolicyViolationType(BuildViolationType(
      d->content_security_policy_violation_type)))
  .build();

Gdy już ustaliliśmy, jakie informacje chcemy udostępniać, musieliśmy sprawdzić, gdzie w Chromium możemy je znaleźć.

Krok 3. Wykrywanie problemów

Aby udostępnić informacje protokołowi narzędzi deweloperskich w Chrome (CDP) w formacie opisanym w ostatniej sekcji, musieliśmy znaleźć miejsce, w którym informacje były faktycznie dostępne w backendzie. Na szczęście kod CSP miał już wąskie gardło używane w trybie tylko raportowania, do którego mogliśmy się podłączyć: ContentSecurityPolicy::ReportViolation zgłasza problemy do (opcjonalnego) punktu końcowego raportowania, który można skonfigurować w nagłówku HTTP CSP. Większość informacji, które chcieliśmy raportować, była już dostępna, więc nie musieliśmy wprowadzać dużych zmian w backendzie, aby nasze instrumenty działały.

Krok 4. Zapisz i wyświetl problemy

Niewielką komplikacją jest fakt, że chcieliśmy też zgłaszać problemy, które wystąpiły przed otwarciem Narzędzi deweloperskich, podobnie jak w przypadku komunikatów konsoli. Oznacza to, że nie zgłaszamy problemów bezpośrednio do interfejsu, ale używamy pamięci, która jest wypełniana problemami niezależnie od tego, czy Narzędzia deweloperskie są otwarte. Po otwarciu Narzędzi deweloperskich (lub podłączeniu dowolnego innego klienta CDP) wszystkie wcześniej zarejestrowane problemy można odtworzyć z pamięci.

To zakończyło pracę nad backendem. Teraz musieliśmy się skupić na tym, jak wyświetlić problem we frontendzie.

Krok 5. Projektowanie tekstu dotyczącego problemów

Projektowanie tekstu problemów to proces, który obejmuje kilka zespołów oprócz naszego. Często korzystamy na przykład ze statystyk od zespołu, który wdraża funkcję (w tym przypadku byłby to zespół CSP), i oczywiście od zespołu DevRel, który projektuje sposób, w jaki programiści stron internetowych mają radzić sobie z określonym typem problemu. Tekst zgłoszenia jest zwykle dopracowywany, aż do momentu jego zakończenia.

Zwykle zespół Narzędzi deweloperskich zaczyna od wstępnego szkicu tego, co sobie wyobraża:


## Header
Content Security Policy: include all sources of your resources in content security policy header to improve the functioning of your site

## General information
Even though some sources are included in the content security policy header, some resources accessed by your site like images, stylesheets or scripts originate from sources not included in content security policy directives.

Usage of content from not included sources is restricted to strengthen the security of your entire site.

## Specific information

### VIOLATED DIRECTIVES
`img-src 'self'`

### BLOCKED URLs
https://imgur.com/JuXCo1p.jpg

## Specific information
https://web.dev/strict-csp/

Po iteracji otrzymaliśmy:

ALT_TEXT_HERE

Jak widać, zaangażowanie zespołu ds. funkcji i DevRel sprawia, że opis jest o wiele bardziej przejrzysty i precyzyjny.

Problemy z CSP na stronie można też znaleźć na karcie poświęconej naruszeniom CSP.

Debugowanie problemów z typami zaufanymi

Praca z TT na dużą skalę może być trudna bez odpowiednich narzędzi dla programistów.

Ulepszone drukowanie w konsoli

Podczas pracy z zaufanymi obiektami chcemy wyświetlać co najmniej tyle samo informacji, co w przypadku ich niezaufanych odpowiedników. Niestety, obecnie podczas wyświetlania zaufanego obiektu nie są wyświetlane żadne informacje o opakowanym obiekcie.

Dzieje się tak, ponieważ wartość wyświetlana w konsoli jest domyślnie pobierana z wywołania .valueOf() na obiekcie. Jednak w przypadku typu zaufanego zwracana wartość nie jest zbyt przydatna. Zamiast tego chcielibyśmy mieć coś podobnego do tego, co otrzymujesz, gdy dzwonisz pod numer .toString(). Aby to osiągnąć, musimy zmodyfikować V8 i Blink, aby wprowadzić specjalną obsługę obiektów zaufanych typów.

Chociaż ze względów historycznych ta niestandardowa obsługa była wykonywana w V8, takie podejście ma istotne wady. Istnieje wiele obiektów, które wymagają niestandardowego wyświetlania, ale ich typ jest taki sam na poziomie JS. Ponieważ V8 to czysty JS, nie może rozróżniać pojęć odpowiadających interfejsowi Web API, takich jak typ zaufany. Z tego powodu V8 musi poprosić o pomoc w ich rozróżnieniu swój komponent osadzający (Blink).

Dlatego przeniesienie tej części kodu do Blink lub dowolnego elementu osadzającego wydaje się logicznym wyborem. Oprócz rozwiązania problemu z ekspozycją, takie rozwiązanie ma wiele innych zalet:

  • Każdy wektor dystrybucyjny może mieć własne generowanie opisu
  • Zdecydowanie łatwiej jest wygenerować opis za pomocą interfejsu Blink API.
  • Blink ma dostęp do pierwotnej definicji obiektu. Jeśli więc użyjemy .toString() do wygenerowania opisu, nie ma ryzyka, że .toString() zostanie zdefiniowany ponownie.

Przerwanie w przypadku naruszenia (w trybie tylko raportowania)

Obecnie jedynym sposobem debugowania naruszeń TT jest ustawienie punktów przerwania w wyjątkach JS. Ponieważ wymuszone naruszenia TT będą wywoływać wyjątek, ta funkcja może być przydatna. Jednak w rzeczywistych sytuacjach potrzebujesz bardziej szczegółowej kontroli nad naruszeniami TT. W szczególności chcemy, aby zatrzymanie następowało tylko w przypadku naruszeń TT (a nie innych wyjątków), również w trybie tylko raportowania, oraz aby rozróżniane były różne typy naruszeń TT.

Narzędzia deweloperskie obsługują już wiele różnych punktów przerwania, więc architektura jest dość rozszerzalna. Dodanie nowego typu punktu przerwania wymaga zmian w backendzie (Blink), CDP i frontendzie. Powinniśmy wprowadzić nowe polecenie CDP, nazwijmy je setBreakOnTTViolation. To polecenie będzie używane przez frontend, aby poinformować backend, na jakich naruszeniach TT powinien się zatrzymać. Backend, a w szczególności InspectorDOMDebuggerAgent, udostępni „sondę” onTTViolation(), która będzie wywoływana za każdym razem, gdy wystąpi naruszenie TT. Następnie InspectorDOMDebuggerAgent sprawdzi, czy to naruszenie powinno wywołać punkt przerwania, a jeśli tak, wyśle wiadomość do frontendu, aby wstrzymać wykonywanie.

Co zostało zrobione i co dalej?

Od czasu wprowadzenia opisanych tu problemów karta Problemy uległa pewnym zmianom:

W przyszłości planujemy używać karty Problemy do wyświetlania większej liczby problemów, co w dłuższej perspektywie pozwoli na odciążenie konsoli od nieczytelnych komunikatów o błędach.

Pobieranie kanałów podglądu

Rozważ użycie Chrome Canary, Dev lub Beta jako domyślnej przeglądarki do programowania. Te kanały wersji testowej dają dostęp do najnowszych funkcji Narzędzi deweloperskich, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i pomagają znajdować problemy w witrynie, zanim zrobią to użytkownicy.

Skontaktuj się z zespołem Narzędzi deweloperskich w Chrome

Skorzystaj z poniższych opcji, aby omówić nowe funkcje, aktualizacje lub inne kwestie związane z Narzędziami deweloperskimi.