Anúncio

Serial(ize) killers, czyli jak popsuliśmy API

Espeo Software
30 de Mar de 2023
Anúncio

Mais conteúdo relacionado

Similar a Serial(ize) killers, czyli jak popsuliśmy API(20)

Anúncio

Serial(ize) killers, czyli jak popsuliśmy API

  1. SERIAL(IZE) KILLERS czyli jak popsuliśmy API Piotr Horzycki / Espeo Software / peterdev.pl
  2. PROSTA ENCJA class Product { public function __construct( public readonly string $id, public readonly string $name, public readonly string $price, public readonly string $editedBy, ) { } }
  3. SERIALIZE(), CZYLI ZŁO $code = serialize($product); O:7:"Product":4:{s:2:"id";s:3:"123";s:4:"name";s:8:"Chainsaw"; s:5:"price";s:10:"EUR 123.45";s:8:"editedBy";s:4:"John";} $product = unserialize($code);
  4. MAŁY PATCH, DUŻY PROBLEM ramsey/uuid 4.2.1 ramsey/uuid 4.2.2 $id = RamseyUuidUuid::uuid4(); C:35:"RamseyUuidLazyLazyUuidFromString":36:{23dc5ed8-4b73-4df8-9421-95e5c07a58e5} O:35:"RamseyUuidLazyLazyUuidFromString":1:{s:6:"string";s:36:"4fdf0133-12be-4089-8fe5-45b4a3e2b
  5. PROBLEM Z BEZPIECZEŃSTWEM https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=unserialize https://redfoxsec.com/blog/insecure-deserialization-in-php/ https://owasp.org/www- community/vulnerabilities/PHP_Object_Injection
  6. KIEDYŚ TO BYŁO... { "example": { "key": "value" } } $request = json_decode(file_get_contents('php://input')); $price = $request['price']; $name = $request['name'] ?? '';
  7. KIEDY WYMYŚLONO SERIALIZER $product = new Product('123', 'Chainsaw', 'EUR 123.45', 'John'); $json = $serializer->serialize($product); { "id": "123", "name": "Chainsaw", "price": "EUR 123.45", "editedBy": "John" } $product = $serializer->deserialize($requestBody, Product::class, 'json');
  8. KIEDY CHCEMY JESZCZE ORM class Product { public function __construct( #[ORMId, ORMColumn] public readonly string $id, #[ORMColumn] public readonly string $name, #[ORMColumn] public readonly string $price, #[ORMColumn] public readonly string $editedBy, ) { } }
  9. KIEDY FRONTEND CHCE COŚ ZMIENIĆ class Product { public function __construct( #[ORMId, ORMColumn] public readonly string $id, #[ORMColumn] #[SerializedName("productName")] public readonly string $name, #[ORMColumn] public readonly string $price, #[ORMColumn] public readonly string $editedBy, ) { } }
  10. KIEDY FRONTEND CHCE RÓŻNE DANE class Product { public function __construct( #[ORMId, ORMColumn] #[Groups(['admin', 'cart'])] public readonly string $id, #[ORMColumn] #[Groups(['admin', 'cart'])] #[SerializedName("productName")] public readonly string $name, #[ORMColumn] #[Groups(['cart'])] public readonly string $price, #[ORMColumn] #[Ignore] public readonly string $editedBy, ) { } }
  11. KIEDY FRONTEND CHCE COŚ EKSTRA class Product { // ... public function getUniversalAnswer(): int { return 42; } #[Ignore] public function getImportantBusinessLogic(): int { return 2 * 2; } }
  12. DORZUCANIE RZECZY POD STOŁEM final class SneakyProductNormalizer implements NormalizerInterface { /** @param Product $object */ public function normalize(mixed $object, string $format = null, array $context = []): ar { return [ 'id' => $object->id, 'name' => $object->name, 'price' => $object->price, 'extraField' => 2 * 2, ]; } // ... }
  13. PERFORMANCE 100 pozycji na liście, 301 zapytań SQL...
  14. ROZWIĄZANIE: DTO class ProductAdminListDto { public function __construct( public readonly string $id, public readonly string $name, ) { } public static function fromEntity(Product $product): self { return new self($product->id, $product->name); } }
  15. OBSŁUGA NULLI I WARTOŚCI NIEZAINICJOWANYCH class Product { public string $sku; public ?string $rating = null; }
  16. OBSŁUGA NULLI I WARTOŚCI NIEZAINICJOWANYCH class Product { public string $sku; public ?string $rating = null; } { // "sku" pominięte, chyba że SKIP_UNINITIALIZED_VALUES = false "rating": null // będzie pominięte dla SKIP_NULL_VALUES = true }
  17. HISTORIA TRUDNEJ MIGRACJI: JMS -> SYMFONY { "type": "IP", "value": "127.0.0.1", "active": 123 }
  18. HISTORIA TRUDNEJ MIGRACJI: JMS -> SYMFONY { "type": "IP", "value": "127.0.0.1", "active": 123 } $rule = $jms->deserialize($requestBody, Rule::class, 'json');
  19. HISTORIA TRUDNEJ MIGRACJI: JMS -> SYMFONY { "type": "IP", "value": "127.0.0.1", "active": 123 } $rule = $jms->deserialize($requestBody, Rule::class, 'json'); W PHP mamy active === TRUE...
  20. USZCZELNIAMY TYPY, PO CZYM KLIENT ZGŁASZA BŁĄD... Zadanie soft skill: wytłumacz klientowi, że się pomylił { "currency": "GBP", "amount": "1234" // 🔥 }
  21. CIĘŻKA PRZEPRAWA Z ENCJAMI inne atrybuty JMS/Symfony gąszcz ExclusionPolicy, Expose, Ignore constructor property promotion, nulle itd.
  22. BŁĘDY WALIDACJI TYPÓW W SYMFONY try { $dto = $serializer->deserialize($requestBody, Product::class, 'json', [ DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, ]); } catch (PartialDenormalizationException $e) { $violations = new ConstraintViolationList(); foreach ($e->getErrors() as $exception) { // ... } return $this->json($violations, 400); } https://symfony.com/doc/current/components/serializer.html#collecting-type-errors-while-denormalizing
  23. DODANIE NOWEGO POLA, A INTEGRACJA KLIENTA class Notification { public function __construct( public readonly string $paymentId, public readonly string $status, public readonly string $receivedAt, ) { } }
  24. DEVELOPERS BE LIKE... "He he... broken... wait a minute..."
  25. DODANIE NOWEGO POLA, A INTEGRACJA KLIENTA class NotificationV2 { public function __construct( public readonly string $paymentId, public readonly string $status, public readonly string $receivedAt, public readonly int $version = 2, ) { } }
  26. ŹLE NAZWANE POLE OPCJONALNE Walidacja przechodzi, ale funkcjonalność nie działa Rozwiązanie: ALLOW_EXTRA_ATTRIBUTES = false { "currency": "GBP", "amount": 1234, "optionalFieldWithTypo": "foo" // 🔥 }
  27. POZIOMY TESTÓW API
  28. POZIOMY TESTÓW API nie ma żadnych
  29. POZIOMY TESTÓW API nie ma żadnych assert HTTP 200 (happy path)
  30. POZIOMY TESTÓW API nie ma żadnych assert HTTP 200 (happy path) assert HTTP 200 (błędne dane)
  31. POZIOMY TESTÓW API nie ma żadnych assert HTTP 200 (happy path) assert HTTP 200 (błędne dane) assert JSON contains
  32. POZIOMY TESTÓW API nie ma żadnych assert HTTP 200 (happy path) assert HTTP 200 (błędne dane) assert JSON contains assert JSON path equals
  33. POZIOMY TESTÓW API nie ma żadnych assert HTTP 200 (happy path) assert HTTP 200 (błędne dane) assert JSON contains assert JSON path equals assert database contains
  34. POZIOMY TESTÓW API nie ma żadnych assert HTTP 200 (happy path) assert HTTP 200 (błędne dane) assert JSON contains assert JSON path equals assert database contains assert no side effects
  35. PODZIAŁ ODPOWIEDZIALNOŚCI MIĘDZY WARSTWAMI TESTÓW jednostkowe integracyjne API E2E
  36. DOKUMENTACJA? A KOMU TO POTRZEBNE... rozjazd między OpenAPI a implementacją brak przykładów w dokumentacji ciągłe pytania od frontendowców
  37. TESTY KONTRAKTÓW Spectator (Laravel) Pact.io
  38. PODSUMOWANIE unikać serialize() nawet drobna różnica w API może popsuć integrację testować API, łącznie z nietypowymi sytuacjami DTO jako pośrednik między encjami a endpointami wersjonowanie lub konfiguracja per klient dokumentacja, która żyje
  39. DZIĘKUJĘ :) peterdev.pl
Anúncio