خلاصه کلیدی
Introspection فقط اولین ورودی است. خطر اصلی در resolverهایی است که فیلتر مستأجر را دور میزنند و در کوئریهایی که هزینه محاسباتیشان سقف ندارد.
وقتی GraphQL در یک سازمان استقرا میشود، معمولاً با یک تصمیم خوشبینانه همراه است: «schema ما مستند است، پس ابزارهای خودکار میتوانند کار ما را بکنند.» در عمل، schema فقط شکل است. GraphQL قدرت را به کسی میدهد که کوئری را مینویسد — و آن کسی معمولاً یک ابزار است که فقط یک لایه اضافه میکند و بلافاصله به لایه resolver میرسد.
الگوی اول: resolver بدون فیلتر مستأجر
در یک پلتفرم چندمستاجری (multi-tenant)، تقریباً هر رزولور باید بداند «این رکورد متعلق به کدام سازمان است؟». اگر یک رزولور این فیلتر را اعمال نکند، یک مستأجر میتواند داده مستأجر دیگر را بخواند — حتی اگر هیچجای دیگری در سیستم ضعیف نباشد.
# schema — looks safe at a glance
type Query {
node(id: ID!): Node # Node = User | Invoice | Workspace
}
# The trap: a union return type lets the caller name
# a different concrete type than the one that was resolved.
query {
node(id: "invoice:100241") {
... on Invoice {
id
amount
workspace { name } # leaks another tenant's name
}
}
}الگوی دوم: هزینه محاسباتی بدون سقف
GraphQL به کلاینت اجازه میدهد ساختار درخت را تعیین کند. این ویژگی قدرت زیادی به کلاینت میدهد و — اگر کنترل نشود — یک مسیر ساده DoS. یک کوئری تودرتو روی رابطهای که خودش تودرتو است، میتواند از نظر ریاضی چند هزار رکورد را در یک درخواست بارگذاری کند.
query {
workspace {
projects { # 12 projects
tasks { # 8 tasks each
comments { # 3 comments each
author { # resolve author per comment
posts { # 4 posts per author
comments { ... } # recursion continues
# 12 × 8 × 3 × 4 = 1,152 nodes before depth limits
}
}
}
}
}
}
}راهحل، غیرفعال کردن introspection نیست. این تنها یک آشکارسازی اطلاعات است و در برابر یک مهاجم که schema را از باندل جاوااسکریپت استخراج میکند هیچ دفاعی نمیسازد. دفاع واقعی سه لایه دارد:
- 1تحلیل هزینه ایستا روی هر فیلد در زمان build، نه در زمان اجرا.
- 2توکیندهی به عمق و پیچیدگی کوئری، با سقف سخت که در صورت عبور، درخواست رد میشود.
- 3سقف تعداد رکورد در هر سطح (complexity limit per list field)، نه فقط در کل کوئری.
الگوی سوم: batch abuse و alias bomb
قابلیت `batch` به کلاینت اجازه میدهد چندین کوئری را در یک درخواست بفرستد. این برای صرفهجویی در رفتوبرگشت طراحی شده، اما در عمل یک ضربهکننده است: یک درخواست HTTP میتواند صدها کوئری مستقل را حمل کند و محدودیت نرخ سازمانی — که بر اساس تعداد درخواست HTTP تنظیم شده — عملاً بیاثر میشود.
الگوی چهارم: resolver های نوشتهشده با ORM که query را دور میزنند
در عمل، شایعترین ریشه یافتههای GraphQL ما یک خطای امنیتی نیست — یک اشتباه معماری است. رزولوری که برای یک فیلد اضافی نوشته شده، گاهی یک `prefetch` اضافه میکند تا درخواست بعدی سریعتر باشد. آن prefetch با کلاس مدل ORM نوشته شده و فیلتر tenant را اعمال نمیکند — چون آن برای مسیر داخلی نوشته شده بود، نه مسیر عمومی.
این یافتهها در بازبینی کد بسیار آسانتر از آنچه در تست داینامیک دیده میشوند پیدا میشوند. به همین دلیل، در ارزیابیهای GraphQL ما تست داینامیک همیشه با بازبینی سورس رزولورها همراه است — نه بهعنوان جایگزین، بلکه بهعنوان مکمل.