PXLSecurity LTD., София, България Офанзивна сигурност от 2014[email protected]
GraphQLAPIСигурност на уеб приложения

Сигурност на GraphQL: изключената интроспекция не означава скрита схема

От PXLSec18 март 2025 г.9 мин. четенеОбновено:

GraphQL предоставя гъвкавост на клиентите, която може да бъде използвана и от нападател. Изключването на интроспекцията само по себе си не защитава API: информация за схемата може да се получи по други начини, а сложните заявки могат да изчерпват ресурси. Разглеждаме тези рискове и проверките, които са нужни отвъд скриването на схемата.

Разкриване на схемата без интроспекция

Интроспекцията е официалният начин да бъде помолено едно GraphQL API да опише себе си. Изключването ѝ премахва очевидния път — но грешките при валидация често така или иначе разкриват схемата. Изпратете леко погрешно име на поле и много сървъри отговарят с полезното предложение „може би имахте предвид …?“. Повторете това и нападателят възстановява имената на заявките и мутациите и формите на входните данни, без интроспекцията изобщо да е била включена. В продукционна среда потискайте предложенията за полета и връщайте общи грешки при валидация.

Изчерпване на ресурси по замисъл

GraphQL позволява на клиента да поиска точно каквото иска — включително далеч прекалено много. Три модела превръщат една заявка в тежка работа за сървъра:

  • Неограничено страниране — заявяване на всички записи наведнъж.
  • Псевдоними на полета — заявяване на едно и също скъпо поле много пъти под различни имена в една заявка.
  • Пакетиране на операции — изпращане на масив от операции, така че една HTTP заявка задейства много.

В комбинация те позволяват на една-единствена удостоверена заявка може да принуди повтарящи се сканирания на целия набор от данни и да влоши наличността за всички. Демонстрирали сме една заявка, която връща хиляди записи по този начин.

А обичайните рискове все още важат

GraphQL също изисква права на ниво обект и поле. Всеки път през resolver към защитени данни трябва да прилага съответната политика, за предпочитане чрез общ слой за бизнес логика и контрол на достъпа. Не дублирайте логиката за собственост във всеки resolver и не разчитайте само на удостоверяване в шлюза; отчетете разрешеното споделяне и делегиране.

Контролен списък за укрепване на защитата

  • Изключете интроспекцията и потиснете грешките с предложения за полета в продукционна среда.
  • Налагайте максимални размери на страниците, дълбочина на заявките и цена на заявките; отхвърляйте прекомерно големи пакети.
  • Налагайте правата за обекти и полета по всеки съответен resolver път чрез последователен слой за политики.
  • Ограничавайте честотата според субекта, като отчитате пакетирането.

Пакетирането и слоят на удостоверяване

Още един специфичен за GraphQL капан, който си заслужава да бъде назован: пакетирането на операции може да усили атаките срещу самото удостоверяване. Като обедини много опита за влизане или заявки за токени в една единствена HTTP заявка, нападателят се промъква направо покрай ограниченията на честотата, които броят заявки, а не операции — превръщайки едно извикване в стотици предположения. Ограничавайте честотата според операцията и според субекта, а не според HTTP заявката, и прилагайте към GraphQL мутациите същите защити срещу груба сила и подбор на изтекли данни за достъп, които бихте приложили към REST крайна точка за вход. Гъвкавият език за заявки не ви освобождава от основите; той просто дава на нападателите нов начин да ги заобиколят.

Ограничения на дълбочината и цената на практика

Ограниченията на дълбочината и цената на заявките не са незадължителни удобства за зряло GraphQL API — те са контролите, които спират една специално изготвена заявка да извърши огромна работа. Задайте максимална дълбочина, присвойте цена на скъпите полета и отхвърляйте всичко, което надвишава бюджета. После го тествайте така, както би го направил нападател, с умишлено дълбоки и скъпи заявки, за да потвърдите, че ограниченията действително се задействат, а не просто съществуват в конфигурацията.

Предварително съхранени заявки и списъци с разрешени заявки като защитен механизъм

По-голямата част от злоупотребите, разгледани дотук, споделят една първопричина: сървърът ще изпълни всяка синтактично валидна заявка, която клиентът изпрати. Ограниченията на честотата, таваните на дълбочината и горните граници на цената всички се опитват да овладеят тази отвореност след факта. Съхранените заявки обръщат модела. Вместо да приема произволен текст на операция, сървърът пази регистър от одобрени операции, всяка идентифицирана с контролна сума, а клиентът изпраща само този идентификатор плюс променливите.

{ "id": "<sha256-на-одобрената-операция>", "variables": { "first": 20 } }

Ако идентификаторът не е в регистъра, заявката се отхвърля преди разбор или планиране. Предложенията за полета, усилването чрез псевдоними, дълбоко вложените опити и изброяването в стил интроспекция — всички се сриват, защото нито една от тези операции никога не е била регистрирана. Затова списъкът с разрешени е най-силният единичен контрол за собствено API, при което наборът от клиенти е известен: схемата може да остане вътрешна, а нападателите все пак не могат да подадат операции по собствен замисъл.

Разликата, която си заслужава да бъде направена, е между автоматично съхранени заявки (APQ) — оптимизация на производителността, която все пак регистрира каквото клиентът изпрати първо — и подбран списък с разрешени, изграден от собствените ви клиенти при компилирането. Само второто е контрол по сигурността. APQ, оставен в режима си по подразбиране „регистрирай при първа употреба“, не е списък с разрешени.

Как да укрепите защитата

  • Генерирайте списъка с разрешени от собствените си клиентски билдове в CI и го изпращайте към шлюза; отхвърляйте непознати идентификатори на операции направо.
  • Не позволявайте на продукционната среда да регистрира нови операции по заявка — разделете удобството на APQ от гаранцията на списъка с разрешени.
  • Дръжте променливите валидирани и ограничени дори за одобрените операции; разрешена заявка с неограничена стойност на first все още е разрешена заявка.
  • За публични или чужди API, при които списъкът с разрешени е непрактичен, се връщайте към многослоен анализ на цената и дълбочината като основна защита.

Как всъщност се изчисляват дълбочината и сложността

Ограниченията на дълбочината и цената бяха споменати по-горе като защита; заслужава си да се види как се извежда една цена, защото наивната реализация е лесна за заобикаляне. Анализът на дълбочината брои вложените нива на избор и отхвърля операция над даден праг. Той е евтин, но груб: плоска заявка, която извлича списъчно поле, връщащо хиляди обекти, всеки с по няколко скалара, има ниска дълбочина и висока действителна цена.

Анализът на сложността присвоява тегло на всяко поле и умножава цената на децата по аргумента за страниране, който определя колко родители съществуват. Поле, връщащо връзка с first: 100, трябва да добави приблизително сто пъти цената на под-избора си, не един път. Общата стойност се изчислява статично, преди изпълнение, от заявката и нейните променливи, и се сравнява спрямо бюджет.

цена(списъчно поле) = аргумент_за_брой x (1 + сума от цена(дъщерни полета))

Честите грешки са броене на полета без умножаване по размера на списъка, игнориране на псевдонимите, така че едно и също поле, преброено десет пъти, се оценява като едно, и прочитане на броя от стойност по подразбиране, а не от действителната променлива, подадена по време на работа. Всяка от тези грешки превръща ограничението на цената в украшение.

Как да укрепите защитата

  • Давайте тегло на списъчните полета и полетата с връзки според действителния им аргумент за страниране, разрешен от променливите, а не от стойностите по подразбиране в схемата.
  • Броете дублираните чрез псевдоними копия на едно поле като отделна цена, тъй като изпълнителят ще разреши всяко от тях.
  • Налагайте максимален размер на страницата за всяка връзка, така че неограничена стойност на first изобщо да не може да бъде подадена.
  • Отхвърляйте въз основа на статичната оценка, преди изпълнението да започне, и допълнително измервайте действителната работа на резолверите, за да уловите оценки, които са се оказали оптимистични.

Инжектиране, достигащо през резолверите до слоя на данните

GraphQL е транспорт и система от типове; той не е изолирана среда (sandbox). Резолверът е обикновен сървърен код, а аргументът на поле е входни данни, контролирани от нападателя, които често завършват в заявка към база данни, филтър за търсене, път до файл или извикване на услуга по-надолу. Системата от типове ограничава формата на един аргумент — String е низ — но не казва нищо за съдържанието му, така че класическите класове инжектиране пътуват направо през добре типизирана схема.

Два модела заслужават особено внимание. Първият е свободно задаван обект за филтриране или сортиране, който се преобразува директно в заявка към базата данни. Той може да позволи на клиента да влияе върху условията, реда или избора на полета. Вторият е идентификатор, който резолверът добавя чрез конкатенация към сурова заявка. Типизирането в GraphQL не гарантира безопасност на заявката към базата данни.

Как да укрепите защитата

  • Третирайте всеки аргумент на резолвер като недоверен; използвайте параметризирани заявки и обвързани идентификатори на нивото на данните, никога слепяне на низове.
  • Моделирайте филтрите като изрични, изброени входни типове с известни оператори, вместо да приемате произволни обекти, които се разгъват в заявка.
  • Валидирайте съдържанието на скаларите, не само типа им — дължина, набор от символи и формат — с персонализирани скалари, където това помага.
  • Прилагайте данни за достъп до базата данни с минимални необходими права за всяка услуга, така че инжектиране в резолвер да не може да достигне данни, които услугата никога законно не докосва.

Обработка на грешките и защита срещу изтичане на информация

Грешките са най-тихият канал за разкриване в едно GraphQL API. Обработката на грешките по подразбиране в много сървъри връща стекови следи, имена на резолвери, съобщения на основните изключения и проблемния път в отговора. Комбинирани с поведението с предложения за полета, разгледано по-горе, подробните грешки позволяват на нападателя да възстанови вътрешното устройство, да научи с коя сървърна част говори дадено поле и да различи „няма оторизация“ от „не съществува“ — разлика, която сама по себе си разкрива структура.

GraphQL връща и частични резултати: един отговор може да носи data за полетата, които са се разрешили, и масив errors за онези, които са се провалили. Това е полезно, но означава, че провал на оторизацията при едно вложено поле не бива да се разлива върху по-широкия обект, а съобщението за грешка не трябва да описва защо един резолвер се е провалил с думи, които нападателят може да извлече.

Как да укрепите защитата

  • Връщайте общи, устойчиви съобщения за грешки към клиентите и журналирайте подробностите от страна на сървъра спрямо идентификатор за съотнасяне; никога не сериализирайте стекови следи или текст на изключения в отговори в продукционна среда.
  • Направете провалите на оторизацията неразличими от отсъствие там, където самото съществуване на ресурс е чувствително.
  • Премахвайте или прикривайте блока extensions, който някои рамки използват, за да прикачат метаданни за отстраняване на грешки, преди отговорите да напуснат шлюза.
  • Известявайте за скокове в честотата на грешките и за серии от печатни грешки, задействащи предложения за полета, които често сигнализират, че схемата се картографира в момента.

Толкова ли е затворено вашето GraphQL API, колкото мислите?

Нашето тестване за проникване в API изследва разкриването на схемата, изчерпването на ресурси и оторизацията на нивото на резолверите — проблемите, които скенерите пропускат.

Определете обхвата на API тест