Jedna linia kodu może zniweczyć cały Twój model kontroli dostępu. Przekazanie body żądania bezpośrednio do aktualizacji bazy danych sprawia, że dajesz klientowi pióro, którym może przepisać Twój schemat. To jest Mass Assignment. To nie jest egzotyczny błąd ani przypadek brzegowy. To błąd projektowy, który pojawia się wszędzie tam, gdzie API traktuje payload jako własną politykę aktualizacji.

await db.users.update(req.params.id, { ...req.body });

Wygląda to czysto. Oszczędza pisania. Ale to klient kontroluje klucze. Atakujący może dodać "role": "admin", "accountId": "someone_else" lub "credit": 99999 do zwykłej aktualizacji profilu. Twoja warstwa walidacji może sprawdzać, czy te wartości są ciągami znaków lub liczbami i uznać, że wyglądają w porządku. Poprawność nie jest jednak tożsama z autoryzacją. Użytkownik może mieć prawo do posiadania danego rekordu, ale nie oznacza to, że powinien mieć prawo do edycji każdego pola w jego obrębie.

Jak w rzeczywistości wygląda Mass Assignment

Niebezpieczeństwo kryje się w wygodzie. Frameworki i ORM-y sprawiają, że mapowanie kluczy JSON bezpośrednio na kolumny bazy danych jest banalnie proste. Robiąc to, mówisz bazie danych, aby ufała klientowi w kwestii tego, co powinno ulec zmianie, a nie tylko jak ma się to zmienić.

Użytkownik aktualizujący swój profil może przesłać poprawne dane dla displayName i bio, ale przy okazji przemycić role lub balance. Jeśli Twój kontroler po prostu przekazuje ten obiekt dalej, baza danych zapisze wszystko. Walidacja wyłapie błędne formaty danych, ale rzadko wyłapuje złośliwe klucze. Reguła biznesowa mówiąca, że „ten użytkownik może aktualizować swój profil”, staje się blankietowym uprawnieniem do każdej kolumny w wierszu.

Rozwiązaniem nie jest więcej walidacji. Jest nim surowsza architektura.

Trzy bramy

Bezpieczna mutacja przechodzi przez trzy oddzielne kontrole, zanim w ogóle dotknie pamięci masowej.

Dozwolone pola (Allowlist)

Zacznij od zdecydowania, na które klucze w ogóle będziesz zwracać uwagę. Jeśli pole nie znajduje się na liście dozwolonych (allowlist), odrzuć żądanie lub usuń ten klucz. To odwraca domyślne podejście: nowe kolumny w bazie danych są niezapisywalne, dopóki programista nie udostępni ich jawnie. Schematy ewoluują w czasie. Współpracownik dodaje stripeCustomerId, departmentBudget lub flagę isVerified. Dzięki liście dozwolonych pól te nowe kolumny są automatycznie chronione przed zapisem ze strony klienta. Bez niej każda nowa kolumna staje się przypadkową powierzchnią ataku API.

Poprawne wartości

Gdy już wiesz, które pola są dozwolone, sprawdź, czy wartości mają sens. Czy ciąg znaków strefy czasowej jest faktycznie rozpoznawalną strefą? Czy e-mail jest sformatowany poprawnie? Czy liczba mieści się w rozsądnym zakresie? To kwestia higieny danych. Zapobiega to wprowadzaniu śmieci do systemu, ale nie powstrzymuje nadużyć. Doskonale poprawny ciąg "admin" w polu role nadal jest niebezpieczny, jeśli wyśle go niewłaściwa osoba.

Autoryzowane przejścia

To brama, którą większość zespołów pomija, a to właśnie tutaj kryje się prawdziwa ochrona. Zadaj szczegółowe pytanie: czy ten konkretny aktor ma uprawnienia do zmiany tego konkretnego pola w tym konkretnym rekordzie? Nie pytaj: „czy użytkownik jest administratorem?” ani „czy użytkownik ma zakres write:users?”. Zapytaj raczej: „czy ten użytkownik może zmienić swój własny displayName, ale nigdy swój accountId?”. Autoryzacja na poziomie poszczególnych pól zapobiega sytuacji, w której szerokie uprawnienie, takie jak „Edytor” lub „Użytkownik”, staje się kluczem uniwersalnym do każdej właściwości w wierszu.

Budowanie funkcji Patch

Połącz trzy bramy w jeden potok (pipeline). Gdy przyjdzie żądanie typu patch, przepuść je przez te etapy w odpowiedniej kolejności.

Po pierwsze, przefiltruj dane wejściowe względem swojej listy dozwolonych pól (allowlist). Jeśli role nie jest dozwolonym polem dla tego endpointu, przerwij proces natychmiast. Nie ma sensu walidować ani autoryzować wartości, której nigdy nie powinieneś był otrzymać.

Po drugie, zweryfikuj dozwolone wartości. Sprawdź typy, formaty i reguły biznesowe. Pole lokalizacji musi być ciągiem znaków odpowiadającym realnej strefie czasowej. URL awatara musi być poprawnym URI o określonej długości.

Po trzecie, autoryzuj akcję. Zweryfikuj, czy aktor jest właścicielem docelowego rekordu lub czy posiada dokładnie takie uprawnienie, jakie jest wymagane dla tego pola. Własność jest dobrą domyślną zasadą dla danych osobowych, ale niektóre pola nadal wymagają dodatkowych barier. Użytkownik może być właścicielem swojego profilu, ale tylko administrator rozliczeń powinien mieć dostęp do taxRegion.

Po czwarte, znormalizuj dane. Usuń zbędne spacje, skoryguj wielokrotne spacje, zamień e-maile na małe litery lub usuń znaki sterujące. Zrób to po walidacji, ale przed zapisem, aby nie porównywać „brudnych” ciągów znaków podczas sprawdzania autoryzacji.

Jeśli dane wejściowe nie przejdą któregokolwiek z etapów weryfikacji, odrzuć całą mutację. Nie stosuj częściowo bezpiecznych pól i po cichu odrzucaj te błędne. Mieszana odpowiedź uczy klientów „rozpylania” każdego możliwego klucza, aby sprawdzić, co zadziała. Zawodź jawnie.

Przypadki brzegowe, które naprawdę mają znaczenie

Mechanizmy obronne przed mass assignment zależą od szczegółów, które testy jednostkowe często pomijają.

Duplikujące się klucze JSON. Atakujący mogą przesyłać ładunki (payloads) takie jak {"role": "user", "role": "admin"}. W zależności od parsera HTTP i frameworka, druga wartość może nadpisać pierwszą, zanim kod aplikacji zobaczy obiekt. Przetestuj to zachowanie na poziomie parsera. Jeśli Twój framework po cichu akceptuje ostatni klucz, Twoja lista dozwolonych (allowlist) może widzieć "user", podczas gdy baza danych otrzyma "admin".

Zagnieżdżone obiekty, wartości null i tablice. Nie zakładaj, że ładunek jest płaski. Klient może umieścić ograniczone pole wewnątrz zagnieżdżonego obiektu, np. { "profile": { "role": "admin" } }. Twoja lista dozwolonych musi działać rekurencyjnie, jeśli robi to Twój schemat. Podobnie, zdecyduj, jak obsługujesz null. Czy oznacza to „zignoruj to pole”, czy „usuń to pole”? A jeśli oczekiwana jest tablica, czy Twój walidator odrzuca nieoczekiwane struktury, czy rzutuje pojedynczy obiekt na tablicę i przepuszcza go dalej?

Normalizacja Unicode. Dwa ciągi znaków mogą wyglądać dla człowieka identycznie, będąc jednocześnie różnymi sekwencjami bajtów. Użytkownik może wysłać znak złożony é lub znak rozłożony e wraz z łączącym znakiem akcentu. Jeśli Twoja weryfikacja uprawnień normalizuje dane raz, a warstwa składowania robi to inaczej, możesz skończyć z niespójnymi danymi lub, co gorsza, z luką pozwalającą na obejście zabezpieczeń (bypass), gdzie kolizja nazw użytkowników prześlizgnie się przez Twoją logikę. Normalizuj wcześnie i rób to spójnie.

Wyścigi (Race conditions). Decyzje o autoryzacji nie są zamrożonymi klatkami obrazu. Dzieją się w konkretnym punkcie w czasie. Dwa żądania mogą odczytać ten sam rekord, oba mogą stwierdzić, że aktor ma uprawnienia do zapisu, i oba mogą wydać polecenie aktualizacji. W międzyczasie stan lub uprawnienia aktora mogły ulec zmianie. Zawsze stosuj aktualizacje bazy danych z warunkiem opartym na numerze wersji lub wartości automatu stanów. Użyj czegoś w stylu UPDATE users SET ... WHERE id = ? AND version = 5. Jeśli wiersz zmienił się od czasu odczytu, zapis się nie powiedzie. Obsłuż błąd poprzez ponowienie próby lub odrzucenie operacji. Zapobiega to uszkodzeniu danych przez nieaktualne (stale) sprawdzenia autoryzacji.

Monitoruj to, co istotne

Nie możesz zabezpieczyć tego, czego nie widzisz. Buduj logowanie audytowe wokół decyzji, a nie tylko samej akcji.

Loguj ID aktora i ID celu. Loguj dokładne nazwy pól, które zostały zaakceptowane, oraz te, które zostały odrzucone. Loguj wersję polityki, która podjęła decyzję, oraz końcowy wynik. Jeśli użytkownik nagle zacznie otrzymywać odrzucenie pola role podczas aktualizacji profilu, chcesz o tym wiedzieć natychmiast.

Nigdy nie loguj tokenów bearer. Nigdy nie wrzucaj całych ciał żądań do logów. Ścieżka audytowa powinna pomagać w badaniu nadużyć, a nie stawać się repozytorium poświadczeń i danych osobowych.

Jedyna zasada, której potrzebujesz

Ciało żądania proponuje dane. Nigdy nie definiuje ono własnych uprawnień. Klient może poprosić o cokolwiek. Twój serwer decyduje, pole po polu i wiersz po wierszu, co może trafić do trwałego składowania. Buduj swoje poprawki z myślą o tym rozdzieleniu, a mass assignment stanie się problemem, który rozwiązałeś na długo przed tym, zanim dotarł do warstwy autoryzacji.