Każda linia kodu ma znaczenie

O tym jak trzy linie kodu mogą wprowadzić poważne luki w zabezpieczeniach

This article is also available in English on LinkedIn.

Wyobraź sobie, że zacząłeś pracę nad nowym projektem. Od czasu do czasu poza typowym realizowaniem zadań badasz repozytoria, aby poznać jak system działa od środka. Czasami pojawia się takie dziwne(trudne do zdefiniowania) uczucie. Uczucie, że coś jest nie do końca w porządku – więc zaczynasz zagłębiać się w kod i testujesz go na różne sposoby.

Weźmy pod uwagę przykład kodu, który znalazłem w jednym z projektów. Wyglądał mniej-więcej tak:

router.get('/image/:url', (req, res) => {
  request
    .get(req.params.url)
    .pipe(res);
});

Ten kod miał za zadanie pobrać obrazek(hostowany na innym serwerze) i wysłać go do klienta(przeglądarki) tak, aby wyglądał jakby był utrzymywany na kontrolowanej przez nas domenie(myDomain.com).

Na przykład, gdy weszliśmy pod adres URL:

https://myDomain.com/image/https://another.domain/thepicture.jpg

Użytkownik zobaczył by obrazek thepicture.jpg, który oryginalnie hostowany był na another.domain jako hostowany na naszej domenie: myDomain.com.

Wspomnę tylko, że:

  • po pierwsze – biblioteka do requestów wspiera zarówno linki HTTP jak i HTTPS,
  • po drugie – istnieje warstwa cache pomiędzy naszym kodem a klientem, która powoduje, że nie wykonujemy (niepotrzebnie) pobrania tego samego zasobu wiele razy.

Tak. Ten kod potrafi także więcej. Ten kod to niebo dla tak zwanych script kiddies – w zasadzie dla domorosłych hakerów i „złych aktorów” również.

Sprawdźmy zatem na jakie zagrożenia ten kod nas naraża.

  • W miejsce adresu źródłowego obrazka możesz podać cokolwiek chcesz. Możesz tam umieścić złośliwy skrypt JS. Przygotować prosty kod HTML z fragmentem JS, który (na przykład) skopiuje ciasteczka i wyśle je na serwer hakera, a potem zrobi przekierowanie na prawdziwy obrazek. Aby użytkownik nawet nie zauważył, że stało się coś dziwnego. Taki adres URL haker może wysłać do kogoś ze wsparcia naszgo serwisu (pisząc na przykład, że coś nam z danym obrazkiem nie działa). Domena nie wzbudzi zapewne podejrzeń – w końcu jest to jedna z domen naszego serwisu. Po wejściu na spreparowany adres URL nasz support nawet nie zauważy, że ktoś w tym momencie zdobył Cookiesy dla domeny myDomain.com!
  • Można wykorzystać taki adres jako proxy dla plików – aby serwować wirusy lub inne złośliwe oprogramowanie. Wyobraź sobie jak zareagują użytkownicy(i nasz biznes) gdy nasza domena myDomain.com zostanie dopisana na blacklistę ze złośliwymi stronami. Zauważą oni wtedy taki komunikat:
Image for post
Komunikat, który pojawi się po dopisaniu domeny myDomain.com do listy złośliwych domen
  • I największy problem – w miejsce adresu obrazka możemy podać zasoby dostępne w sieci wewnętrznej (na przykład intra.my.company/importantDoc.pdf). Pliki, dokumenty, kod i wiele więcej. Można w ten sposób zinfiltrować zasoby wewnętrzne. Gdy haker zgadnie domenę lokalną lub adres IP z danymi, które są krytyczne dla naszego biznesu – mamy gotowy przepis na wyciek danych. GDPR nie będzie po naszej stronie(w zasadzie słusznie).

Co zrobić, gdy potrzebujemy takiego proxy?

  • Nigdy nie pozwalaj na przekierowanie wszystkiego. Zawsze używaj sumy kontrolnej(hasha z „solą”) gdy generujesz tego typu adresy URL. Dzięki temu nie będzie możliwości, aby dodawać dodatkowe – złośliwe – parametry.
  • Zawsze sprawdzaj typ treści, którą pobierasz. Jeżeli chcesz zrobić proxy dla obrazków – pobierz tylko takie pliki, których typ (np. MIME) jest obrazkowy. Dodawaj też nagłówek z typem treści do każdej odpowiedzi dla użytkownika(Content-Type). Przeglądarka powinna wykorzystać ten nagłówek i nie wywołać kodu JS, gdy Content-Type to image/jpeg.
  • Izoluj sieć wewwnętrzną od maszyn, które wykonują kod(np. kontenerów). Dwa razy zastanów się czy potrzebujesz mieć dostęp z aplikacji do (na przykład) Intranetu. Jeżeli potrzebujesz – wykorzystaj jakiś niestandardowy port lub wewnętrzne proxy, które będzie monitorować requesty i pozowli na wykonanie tylko ograniczonego typu i liczby zapytań do serwisów wewnętrznych.

Przykład kodu, który powinien ograniczyć liczbę luk

const crypto = require('./crypto'); // library that can encrypt and decrypt some text
const validator = require('./validator'); // library that can perform validation on stream
const app = express();
const IMAGE_FROM_TRUSTED_SOURCE = 'https://trusted.domain/thepicture.jpg';
router.get('/', (req, res) => {
  const {hash, cryptedUrl} = crypto.encrypt(IMAGE_FROM_TRUSTED_SOURCE);
  const imageUrl = `/image/v1/${hash}/${cryptedUrl}`;
res.end(`<img src="${imageUrl}" alt="Image that can be trusted" />`);
});
router.get('/image/v1/:hash/:cryptedUrl', (req, res) => {
  const cryptedUrl = request.params.cryptedUrl;
  const hash = request.params.hash;
try {
    const url = crypto.decrypt(cryptedUrl, hash); // crypto has it's own secret/salt that is combined with "public" secret(hash)
request
      .get(url)
      .pipe(validator.validateContentType(['image/jpg', 'image/png']))
      .pipe(res);
  } catch (error) {
    console.error('Someone is probably doing something naughty', error);
res.end();
  }
});

Jak możemy zauważyć, gdy próbujemy stowrzyć adres URL najpierw tworzymy hash, który uniemożliwi hakerowi tworzenie/zmianę parametrów. Wykorzystaj w tym celu bibliotekę, która jest sprawdzona i rozwijana. Na przykład Node.JS ma Crypto.

Gdy użytkownik chce otworzyć Twój adres URL, haker nie może podmienić mu na przykład adresu źródłowego, gdyż jest on weryfikowany w fazie „decrypt” i zakończy się ona niepowodzeniem. Enkrypcja/hashowanie powinno wykorzystywać tak zwaną sól, która nigdy nie powinna być udostępniona publicznie.

Teraz jesteśmy już bezpieczni!

Miłego dnia!

P.S. Podczas gdy piszesz kod nie zapominaj o jego testowaniu. Przeczytaj o tym jak tworzyć niezawodne oprogramowanie z użyciem TDD(Test-Driven Development).