Blog

Wiedza

Ochrona wrażliwych danych podczas serializacji

block image
lsb bulb

Potrzebujesz bezpiecznej aplikacji internetowej?

REST API

Aktualnie coraz częściej spotykanym podejściem przy realizacji aplikacji internetowych jest rozdzielenie części klienckiej od części zapleczowej. Zwykle mamy do czynienia z dwoma rodzajami aplikacji:

  • aplikacja typu frontend, klient - przygotowana np. we Vue.JS
  • aplikacja typu backend, serwer - zrealizowana np. w PHP Symfony5

Te dwie odrębne aplikacje muszą się ze sobą komunikować, najlepiej w możliwie najprostszy sposób. W 2000 roku opracowano podstawy tzw. podejścia RESTful API, a więc komunikacji z użyciem protokołu HTTP, bez konieczności instalacji dodatkowych bibliotek i zależności. Ten sposób komunikacji stosowany jest właśnie przy opisanym wyżej podejściu w architekturze klient-serwer. Kwestię REST API zgłębimy może przy okazji odrębnego artykułu, teraz przejdziemy do kwestii samych danych.

W przypadku REST API dane dla aplikacji klienckiej muszą być udostępniane w formie usystematyzowanej, spójnej, używa się do tego celu formatu JSON lub XML. Bardziej popularny jednak format JSON, z uwagi na bardziej czytelną postać i mniejszą objętość. Format JSON nazywany jest również lekkim formatem wymiany danych.

Jeżeli mieliście już do czynienia z programowaniem obiektowym i z systemami mapowania relacyjnego ORM, to zapewne już wiecie, że wszystko kręci się wokół obiektów. Tworzymy klasę, tzw. encję, mapujemy jej zawartość na tabele w bazie danych i zawsze pracujemy w oparciu w obiekty. Udostępnienie czy też potocznie mówiąc wystawienie danych poprzez REST API jest po prostu wydłużeniem łańcucha danych.

Serializacja danych

W celu udostępnienia danych z obiektów za pomocą REST API musimy dokonać tzw. serializacji, a więc zamiany obiektu na ciąg znaków w określonym formacie wymiany danych. Do tego celu użytkownicy frameworka Symfony wspomagają się wbudowanym mechanizmem serializacji lub korzystają z niezwykle popularnego zewnętrznego bundla: JMSSerializerBundle https://jmsyst.com/bundles/JMSSerializerBundle.

JMSSerializerBundle podobnie jak wbudowany mechanizm serializacji danych we frameworku Symfony pozwala nam zamienić obiekt na ciąg znaków w formacie JSON, ale czy zawsze chcemy udostępniać publicznie wszystkie dane zawarte w ramach obiektu ? Pamiętajmy, że w przypadku architektury typu serwer-klient możliwe jest podejrzenie pełnej komunikacji pomiędzy klientem (aplikacja JavaScript uruchomiona w ramach przeglądarki internetowe) a serwerem. Dlatego warto jest się zastanowić czy serializacji powinny podlegać wszystkie dane dostępne w ramach obiektu?

Pamiętajmy, że w przypadku systemów ORM na obiekt zostaną zmapowane wszystkie dane z tabeli bazy danych. Czy na pewno chcemy, żeby bardziej zaawansowani użytkownicy sklepu internetowego byli w stanie podejrzeć wewnętrzne uwagi handlowców o nich samych? Czy jest to faktycznie możliwe?

Już wyjaśniam, jeżeli dokonamy serializacji całości obiektu zamówienia, zawierającego kolekcję uwag handlowców to klient będzie w stanie podejrzeć ich treść poprzez analizę logów requestów XHR (XMLHttpRequest). Tego typu logi dostępne są w każdej przeglądarce internetowej, a pełną zawartość odpowiedzi serwera na requesty można poznać nawet  pomimo tego, że aplikacje frontendowa (kliencka) nigdzie tych danych nie wyświetli.

Tutaj z pomocą przychodzi nam mechanizm grup serializacyjnych i adnotacji serializera, które możemy użyć przy tworzeniu encji. Decydujemy się na użycie JMSSerializerBundle, w tym wariancie mamy możliwość określenia polityki serializacji. Możemy domyślnie wykluczać wszystkie własności klasy z serializacji lub domyślnie serializować wszystkie dostępne dane.

Zasady wykluczania

Poniższy przykład klasy i użytej polityki serializacji spowoduje wykluczenie z serializacji wszystkich własności przy domyślnej grupie serializacyjnej.

<?php
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;

/**
 * @ExclusionPolicy("all")
 */
class MySuperClass
{
    protected ?int $stage;

    protected ?string $name;
   /**
     * @Expose
     */
    protected ?string $slug;
}

?>

Polityka serializacji

Domyślnie serializacji podlegają wszystkie dostępne własności danej klasy, warto więc zadbać na wstępie o automatycznie wykluczenie wszystkich dostępnych własności. Bezpieczniej jest wykluczyć z serializacji wszystkie dane i następnie włączać do serializacji poszczególne własności niż stosować podejście odwrotnie. Włączenie do serializacji wszystkich własności i wykluczanie wybranych może stwarzać pozorne wrażenie uproszczenia pracy i oszczędności czasu. Nic bardziej mylnego, dodając nowe własności, np. zawierające dane wrażliwe, możemy łatwo zapomnieć o konieczności ich ręcznego wykluczenia z serializacji. 

Pamiętaj! Bezpieczniej jest udostępnić mniej danych, niż udostępnić ich za dużo.

<?php

/**
 * @ExclusionPolicy("all")
 */

class MySuperClass
{

    /**
     * @Expose(if="canExpose(object, context, property_metadata)")
     */
    protected ?string $name;

    /**
     * @Expose(if="canExpose(object, context, property_metadata)")
     */
    protected ?string $slug;
?>

W użytym powyżej przykładzie własność $slug oznaczona jest adnotacją @Expose, oznacza to ręczne udostępnienie zawartości własność $slugi przy serializacji. W rezultacie podczas procesu serializacji całego obiektu MySuperClass otrzymamy jedynie zawartość własności $slug.

Grupy serializacyjne

Grupy serializacyjne działają w pewnym sensie podobnie do grup walidacyjnych znanych z formularzy frameworka Symfony, oczywiście spełniając inne zadanie, jednak zasada działania jest podobna. Jak to działa? Przed rozpoczęciem serializacji musimy wskazać grupę lub kilka grup serializacyjnych, których będziemy używać w procesie serializacji. Wybrane grupy serializacyjne powinny mieć odzwierciedlenie w konfiguracji serializacji zawartej w ramach encji (lub opcjonalnie w formacie yaml). Mechanizm serializacji użyje tych własności, które zawierają się w zadeklarowanej puli grup serializacyjnych naszej aplikacji backendowej.

use JMS\Serializer\Annotation\Groups;

class Article
{
    /** @Groups({“common”}) */
    protected $uuid;

    /** @Groups({“common”, “list”, "details"}) */
    protected $title;

    /** @Groups({“content”, “details”}) */
    protected $title;
}

$serializer->serialize(new Article(), 'json', SerializationContext::create()->setGroups([‘common’, ‘list’]));

Użycie grup serializacji

Wybrane grupy serializacyjne powinny mieć odzwierciedlenie w konfiguracji serializacji zawartej w ramach encji (lub opcjonalnie w formacie yaml). Mechanizm serializacji użyje tych własności, które zawierają się w zadeklarowanej puli grup serializacyjnych naszej aplikacji backendowej. 

Jak jednak możemy użyć grup serializacyjnych do zabezpieczenia danych wrażliwych? Wystarczy mała funkcja pomocnicza, tzw. helper, aktualny kontekst użytkownika i weryfikacja przyznanych ról użytkownikowi. Na podstawie przydzielonych ról użytkownika możemy różnicować pulę grup serializacyjnych. W zależności od tego z jakim użytkownikiem mamy do czynienia zwracane są różne własności tego samego obiektu. Dla przykładu serializowany obiekt zamówienia dla administratora będzie zawierał dodatkowe własności z notatkami wewnętrznymi handlowców, jednocześnie ten sam obiekt serializowany dla klienta sklepu internetowego (zwykłego użytkownika) nie będzie zawierał tych własności.

Ciekawym przykładem wykorzystania grup serializacyjnych jest np. podejście do ukrywania cen np. w systemach B2B, gdzie nie każdy z użytkowników systemu ma mieć wgląd w generowane koszty. W pewnym projekcie EDI klient postawił wymóg ukrywania cen produktów dla niektórych klientów, ukrywanie cen miało odbywać zarówno dla produktów, jak i wszystkich innych obiektów w których były widoczne ceny, wartości lub podsumowania kosztów. Jak zrealizować tego typu zadanie bez przygotowywania nadmiernej ilości logiki?

Wystarczy przygotować wspólny mechanizm dla wszystkich endpointów API, tak aby możliwe było różnicowanie grup serializacyjnych na podstawie kontekstu zalogowanego użytkownika.

$groups = [‘common’];

$showPrice = $this->getUser() ? $this->getUser()->getShowPrices() : false;

if ($showPrice) {
    $groups[] = ‘showPrices’
}

$serializationContext = new SerializationContext::create()->setGroups($groups);
$serializer->serialize(new Article(), 'json', $serializationContext);

Wewnątrz helpera mamy dostępny obiekt użytkownika, przydzielone role i flagi ustawione na koncie użytkownika systemu B2B. Na podstawie tych wszystkich danych ustalana jest pula grup serializacyjnych, która później w ramach kontekstu serializacji przekazywana jest do serwisu serializera. Zamiast powielać grupy serializacyjne w encjach można zastosować podejście małych grup funkcjonalnych.

Tworzymy wówczas bazową część wspólną własności (Common), która ma być zawsze dostępna przy serializacji, natomiast w przypadku różnicowania serializowanych własności tworzymy dodatkowe grupy serializacyjne właściwe dla określonej funkcji albo przydzielonej roli użytkownikowi. Np. ShowPrices. Do tej grupy trafią wszystkie własności encji zawierające ceny. Grupa powinna być wspólna dla wszystkich encji użytych w projekcie. 

Jeżeli użytkownik posiada odpowiednią rolę, helper handleSerialize doda grupę “ShowPrices” do puli grup serializacyjnych, w rezultacie na zasobie API otrzymamy w ramach serializowanych obiektów ich ceny lub też nie. 

Tego typu podejście pozwala w prosty sposób ograniczać globalnie ilość serializowanych własności wszystkich encji w danej aplikacji internetowej. Ewentualne dodanie nowej grupy serializacyjnej nie będzie stanowić już dużego problemu.

use JMS\Serializer\Annotation\Groups;

class Article
{
    /** @Groups({“common”}) */
    protected $uuid;

    /** @Groups({“title”}) */
    protected $title;

    /** @Groups({“content”}) */
    protected $intro;

    /** @Groups({“content”}) */
    protected $content;

    /** @Groups({“content”}) */
    protected $description;
}

 

lsb bulb

Masz pomysł? Porozmawiajmy

Masz pomysł? Opowiedz o swoim projekcie.