Powiedzmy, że kodujesz funkcję, która pobiera dane wejściowe z zewnętrznego interfejsu API MyAPI
.
Ten zewnętrzny interfejs API MyAPI
ma umowę, która stwierdza, że zwróci a string
lub a number
.
Zaleca się, aby ustrzec się przed rzeczy takie jak null
, undefined
, boolean
, itd., Mimo że nie jest częścią API MyAPI
? W szczególności, ponieważ nie masz kontroli nad tym interfejsem API, nie możesz zagwarantować tego za pomocą analizy typu statycznego, więc lepiej być bezpiecznym niż żałować?
Mam na myśli zasadę solidności .
design
api
api-design
web-services
functions
Adam Thompson
źródło
źródło
<!doctype html><html><head><title>504 Gateway Timeout</title></head><body>The server was unable to process your request. Make sure you have typed the address correctly. If the problem persists, please try again later.</body></html>
Odpowiedzi:
Nigdy nie należy ufać wejściom do oprogramowania, niezależnie od źródła. Ważne jest nie tylko sprawdzanie poprawności typów, ale także zakresów danych wejściowych i logiki biznesowej. W komentarzu jest to dobrze opisane przez OWASP
Niezastosowanie się do tego spowoduje w najlepszym wypadku śmieciowe dane, które musisz później wyczyścić, ale w najgorszym przypadku pozostawisz możliwość złośliwych exploitów, jeśli ta usługa zostanie w jakiś sposób naruszona (qv Target hack). Zakres problemów między nimi obejmuje doprowadzenie aplikacji do stanu niemożliwego do odzyskania.
Z komentarzy wynika, że być może moja odpowiedź przydałaby się trochę rozbudowy.
Przez „nigdy nie ufaj wejściom” mam na myśli po prostu, że nie możesz zakładać, że zawsze będziesz otrzymywać ważne i godne zaufania informacje z wcześniejszych lub późniejszych systemów i dlatego zawsze powinieneś dezynfekować te dane wejściowe najlepiej jak potrafisz, lub odrzucić to.
Jeden komentarz pojawił się w komentarzach, które podam jako przykład. Chociaż tak, musisz w pewnym stopniu zaufać swojemu systemowi operacyjnemu, ale nie jest nieuzasadnione, na przykład, odrzucenie wyników generatora liczb losowych, jeśli poprosisz o numer od 1 do 10, a on odpowie „bob”.
Podobnie, w przypadku PO, zdecydowanie powinieneś upewnić się, że Twoja aplikacja akceptuje tylko prawidłowe dane wejściowe z usługi nadrzędnej. To, co zrobisz, gdy nie będzie w porządku, zależy od ciebie i zależy w dużej mierze od faktycznej funkcji biznesowej, którą próbujesz osiągnąć, ale minimalnie zalogowałbyś ją do późniejszego debugowania i w przeciwnym razie upewnisz się, że aplikacja nie pójdzie w stan niemożliwy do odzyskania lub niepewny.
Chociaż nigdy nie możesz poznać wszystkich możliwych danych wejściowych, które może dać ktoś / coś, z pewnością możesz ograniczyć to, co jest dozwolone, w zależności od wymagań biznesowych i wykonać jakąś formę białej listy danych wejściowych na tej podstawie.
źródło
Tak , oczywiście. Ale co sprawia, że uważasz, że odpowiedź może być inna?
Na pewno nie chcesz, aby Twój program zachowywał się w nieprzewidziany sposób, na wypadek gdyby API nie zwróciło tego, co mówi umowa, prawda? Tak przynajmniej można mieć do czynienia z takim zachowaniem jakoś . Minimalna forma obsługi błędów jest zawsze warta (bardzo minimalnego!) Wysiłku i nie ma absolutnie żadnej wymówki, aby nie implementować czegoś takiego.
Jednak ile wysiłku powinieneś zainwestować, aby poradzić sobie z taką sprawą, zależy ona w dużej mierze od przypadku i można na nie odpowiedzieć tylko w kontekście twojego systemu. Często wystarczy krótki wpis w dzienniku i pełne zakończenie działania aplikacji. Czasami lepiej będzie wdrożyć szczegółową obsługę wyjątków, zajmując się różnymi formami „złych” wartości zwracanych, i być może trzeba będzie wdrożyć jakąś strategię awaryjną.
Ale robi to ogromną różnicę, jeśli piszesz tylko wewnętrzną aplikację do formatowania arkuszy kalkulacyjnych, z której może korzystać mniej niż 10 osób i gdzie wpływ finansowy awarii aplikacji jest niewielki, lub jeśli tworzysz nową autonomiczną jazdę samochodem system, w którym awaria aplikacji może kosztować życie.
Dlatego nie ma skrótu do zastanowienia się nad tym, co robisz , stosowanie zdrowego rozsądku jest zawsze obowiązkowe.
źródło
Zasada niezawodności - w szczególności połowa jej „bądź liberalna w tym, co akceptujesz” - jest bardzo złym pomysłem w oprogramowaniu. Został pierwotnie opracowany w kontekście sprzętu, w którym ograniczenia fizyczne sprawiają, że tolerancje techniczne są bardzo ważne, ale w oprogramowaniu, gdy ktoś wysyła ci zniekształcone lub w inny sposób niewłaściwe dane wejściowe, masz dwie możliwości. Możesz to odrzucić (najlepiej z wyjaśnieniem, co poszło nie tak) lub spróbować dowiedzieć się, co to miało znaczyć.
Nigdy, nigdy, nigdy nie wybieraj tej drugiej opcji, chyba że masz zasoby równoważne zespołowi wyszukiwania Google, aby rzucić na swój projekt, ponieważ to jest to, czego potrzeba, aby wymyślić program komputerowy, który wykonuje coś zbliżonego do przyzwoitej pracy w tej konkretnej domenie problemowej. (I nawet wtedy sugestie Google mają wrażenie, że mniej więcej w połowie wychodzą z lewego pola.) Jeśli spróbujesz to zrobić, skończy się to ogromnym bólem głowy, w którym Twój program będzie często próbował interpretować złe wejście jako X, gdy tak naprawdę nadawca miał na myśli Y.
Jest to złe z dwóch powodów. Oczywistym jest to, że wtedy masz złe dane w swoim systemie. Mniej oczywistym jest to, że w wielu przypadkach ani ty, ani nadawca nie zdacie sobie sprawy, że coś poszło nie tak, aż znacznie później w dół drogi, gdy coś wysadza ci w twarz, a potem nagle masz duży, drogi bałagan do naprawienia i nie masz pojęcia co poszło nie tak, ponieważ zauważalny efekt jest tak daleko od pierwotnej przyczyny.
Właśnie dlatego istnieje zasada Fail Fast; oszczędzaj wszystkim zaangażowanym ból głowy, stosując go do swoich interfejsów API.
źródło
Zasadniczo kod powinien być konstruowany tak, aby zachowywał przynajmniej następujące ograniczenia, ilekroć jest to praktyczne:
Po otrzymaniu poprawnego wejścia, wygeneruj prawidłowe wyjście.
Gdy podano prawidłowe dane wejściowe (które mogą, ale nie muszą być poprawne), wygeneruj prawidłowe dane wyjściowe (podobnie).
Jeśli podano nieprawidłowe dane wejściowe, należy je przetwarzać bez żadnych skutków ubocznych poza tymi powodowanymi przez normalne dane wejściowe lub te, które są zdefiniowane jako sygnalizowanie błędu.
W wielu sytuacjach programy będą zasadniczo przechodzić przez różne porcje danych, nie zwracając szczególnej uwagi na to, czy są poprawne. Jeśli takie fragmenty zawierają nieprawidłowe dane, wynik programu prawdopodobnie zawiera nieprawidłowe dane. O ile program nie został specjalnie zaprojektowany do sprawdzania poprawności wszystkich danych i nie zagwarantuje, że nie wygeneruje nieprawidłowych danych wyjściowych, nawet jeśli otrzyma nieprawidłowe dane wejściowe , programy przetwarzające dane wyjściowe powinny dopuszczać możliwość wystąpienia w nich nieprawidłowych danych.
Chociaż wczesna weryfikacja danych jest często pożądana, nie zawsze jest szczególnie praktyczna. Między innymi, jeśli ważność jednego fragmentu danych zależy od zawartości innych fragmentów i jeśli większość danych wprowadzonych do jakiejś sekwencji kroków zostanie odfiltrowana po drodze, ograniczając sprawdzanie poprawności do danych, które je przechodzą wszystkie etapy mogą dać znacznie lepszą wydajność niż próba sprawdzenia wszystkiego.
Ponadto, nawet jeśli jest tylko oczekuje się otrzymać wstępnie zatwierdzone dane program, często dobrze jest mieć go podtrzymywać powyższe ograniczenia i tak ilekroć praktyczne. Powtarzanie pełnej walidacji na każdym etapie przetwarzania często powoduje znaczne obniżenie wydajności, ale ograniczona ilość walidacji potrzebnej do utrzymania powyższych ograniczeń może być znacznie tańsza.
źródło
Porównajmy dwa scenariusze i spróbuj dojść do wniosku.
Scenariusz 1 Nasza aplikacja zakłada, że zewnętrzny interfejs API będzie działał zgodnie z umową.
Scenariusz 2 Nasza aplikacja zakłada, że zewnętrzny interfejs API może źle działać, dlatego dodaje środki ostrożności.
Zasadniczo każdy interfejs API lub oprogramowanie może naruszyć umowy; może być spowodowane błędem lub nieoczekiwanymi warunkami. Nawet interfejs API może mieć problemy w wewnętrznych systemach, powodując nieoczekiwane wyniki.
Jeśli nasz program jest napisany przy założeniu, że zewnętrzny interfejs API będzie przestrzegać umów i uniknie dodawania jakichkolwiek środków ostrożności; kto będzie stroną w obliczu problemów? To my będziemy tymi, którzy napisali kod integracji.
Na przykład wybrane wartości zerowe. Powiedzmy, że zgodnie z umową API odpowiedź nie powinna mieć wartości zerowej; ale jeśli zostanie to nagle naruszone, nasz program spowoduje NPE.
Tak więc uważam, że lepiej będzie upewnić się, że aplikacja zawiera dodatkowy kod, aby rozwiązać nieoczekiwane scenariusze.
źródło
Zawsze powinieneś sprawdzać poprawność danych przychodzących - wprowadzonych przez użytkownika lub w inny sposób - dlatego powinieneś mieć proces do obsługi, gdy dane pobrane z tego zewnętrznego API są nieprawidłowe.
Ogólnie rzecz biorąc, każdy szew, w którym spotykają się systemy pozarządowe, powinien wymagać uwierzytelnienia, autoryzacji (jeśli nie jest zdefiniowany jedynie przez uwierzytelnienie) i walidacji.
źródło
Zasadniczo tak, zawsze należy chronić się przed błędnymi danymi wejściowymi, ale w zależności od rodzaju interfejsu API „ochrona” oznacza różne rzeczy.
W przypadku zewnętrznego interfejsu API dla serwera nie chcesz przypadkowo utworzyć polecenia, które powoduje awarię lub zagraża stanie serwera, więc musisz się przed tym zabezpieczyć.
W przypadku interfejsu API, takiego jak np. Klasa kontenera (lista, wektor itp.), Zgłaszanie wyjątków jest całkowicie dobrym wynikiem, kompromis w stanie instancji klasy może być do pewnego stopnia akceptowalny (np. Posortowany kontener wyposażony w wadliwy operator porównania nie będzie być posortowane), nawet zawieszenie aplikacji może być dopuszczalne, ale kompromis w stanie aplikacji - np. zapis w losowych lokalizacjach pamięci niezwiązanych z instancją klasy - najprawdopodobniej nie.
źródło
Aby wyrazić nieco odmienną opinię: myślę, że dopuszczalne jest po prostu praca z danymi, które otrzymałeś, nawet jeśli narusza to umowę. Zależy to od użycia: jest to coś, co MUSI być ciągiem dla Ciebie, czy jest to coś, co po prostu wyświetlasz / nie używasz itp. W tym drugim przypadku po prostu zaakceptuj to. Mam interfejs API, który potrzebuje tylko 1% danych dostarczonych przez inny interfejs. Nie obchodzi mnie, jakie dane są w 99%, więc nigdy tego nie sprawdzę.
Należy zachować równowagę między „błędami, ponieważ nie sprawdzam wystarczająco danych wejściowych” a „odrzucam prawidłowe dane, ponieważ jestem zbyt surowy”.
źródło
Podejmuję to, by zawsze, zawsze sprawdzać każde wejście do mojego systemu. Oznacza to, że każdy parametr zwracany z interfejsu API powinien zostać sprawdzony, nawet jeśli mój program go nie używa. Mam tendencję do sprawdzania poprawności każdego parametru wysyłanego do interfejsu API. Istnieją tylko dwa wyjątki od tej reguły, patrz poniżej.
Powodem testowania jest to, że jeśli z jakiegoś powodu API / dane wejściowe są niepoprawne, mój program nie może na niczym polegać. Może mój program był powiązany ze starą wersją interfejsu API, która robi coś innego niż to, w co wierzę? Może mój program natrafił na błąd w programie zewnętrznym, który nigdy wcześniej nie miał miejsca. Albo jeszcze gorzej, zdarza się cały czas, ale nikogo to nie obchodzi! Być może haker oszukuje program zewnętrzny, aby zwracał rzeczy, które mogą zaszkodzić mojemu programowi lub systemowi?
Dwa wyjątki od testowania wszystkiego w moim świecie to:
Wydajność po starannym pomiarze wydajności:
Gdy nie masz pojęcia, co zrobić z błędem
Dokładnie, jak dokładnie sprawdzić wartości wejściowe / zwracane, jest ważnym pytaniem. Na przykład, jeśli mówi się, że API zwraca ciąg znaków, sprawdziłbym, czy:
typ danych faktycznie jest ciągiem
i ta długość zawiera się między wartościami min. i maks. Zawsze sprawdzaj ciągi pod kątem maksymalnego rozmiaru, który mój program może obsłużyć (zwracanie zbyt dużych ciągów to klasyczny problem bezpieczeństwa w systemach sieciowych).
Niektóre ciągi powinny być sprawdzane pod kątem „nielegalnych” znaków lub treści, gdy jest to istotne. Jeśli Twój program może później wysłać łańcuch z informacją o bazie danych, dobrym pomysłem jest sprawdzenie ataków bazy danych (poszukiwanie iniekcji SQL). Te testy najlepiej wykonywać na granicach mojego systemu, gdzie mogę dokładnie określić, skąd pochodzi atak, i mogę wcześnie zawieść. Wykonanie pełnego testu wstrzykiwania SQL może być trudne, gdy łańcuchy zostaną później połączone, więc test należy wykonać przed wywołaniem bazy danych, ale jeśli jakieś problemy można wcześnie znaleźć, może być przydatne.
Powodem testowania parametrów wysyłanych do API jest upewnienie się, że otrzymam poprawny wynik. Ponownie wykonanie tych testów przed wywołaniem interfejsu API może wydawać się niepotrzebne, ale wymaga bardzo małej wydajności i może wykryć błędy w moim programie. Dlatego testy są najbardziej cenne przy opracowywaniu systemu (ale obecnie wydaje się, że każdy system jest w ciągłym rozwoju). W zależności od parametrów testy mogą być mniej lub bardziej dokładne, ale często stwierdzam, że często można ustawić dopuszczalne wartości minimalne i maksymalne dla większości parametrów, które mój program mógłby stworzyć. Być może ciąg powinien zawsze mieć co najmniej 2 znaki i mieć maksymalnie 2000 znaków? Minimalna i maksymalna wartość powinna mieścić się w tym, na co pozwala API, ponieważ wiem, że mój program nigdy nie użyje pełnego zakresu niektórych parametrów.
źródło