DevByte
security ops
[ok]ایمن‌سازی کانال ارتباطی …
loading assets000%
رفتن به محتوای اصلی
بازگشت به مقالات
ارزیابی وب۱۴۰۵/۴/۱۱·۹ دقیقه

الگوهای سوءاستفاده از GraphQL که در تست‌ها جا می‌مانند

Introspection فقط اولین ماجراست. کوئری‌های union، عمق توکن‌شده، batch abuse و resolverهایی که فیلتر مستأجر را دور می‌زنند — الگوهایی که فقط با تست نقش‌محور پیدا می‌شوند.

GraphQLDoSTenant Isolation

خلاصه کلیدی

Introspection فقط اولین ورودی است. خطر اصلی در resolverهایی است که فیلتر مستأجر را دور می‌زنند و در کوئری‌هایی که هزینه محاسباتی‌شان سقف ندارد.

وقتی GraphQL در یک سازمان استقرا می‌شود، معمولاً با یک تصمیم خوش‌بینانه همراه است: «schema ما مستند است، پس ابزارهای خودکار می‌توانند کار ما را بکنند.» در عمل، schema فقط شکل است. GraphQL قدرت را به کسی می‌دهد که کوئری را می‌نویسد — و آن کسی معمولاً یک ابزار است که فقط یک لایه اضافه می‌کند و بلافاصله به لایه resolver می‌رسد.

الگوی اول: resolver بدون فیلتر مستأجر

در یک پلتفرم چندمستاجری (multi-tenant)، تقریباً هر رزولور باید بداند «این رکورد متعلق به کدام سازمان است؟». اگر یک رزولور این فیلتر را اعمال نکند، یک مستأجر می‌تواند داده مستأجر دیگر را بخواند — حتی اگر هیچ‌جای دیگری در سیستم ضعیف نباشد.

graphql
# 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
    }
  }
}
union type بدون اعمال فیلتر tenant_id

الگوی دوم: هزینه محاسباتی بدون سقف

GraphQL به کلاینت اجازه می‌دهد ساختار درخت را تعیین کند. این ویژگی قدرت زیادی به کلاینت می‌دهد و — اگر کنترل نشود — یک مسیر ساده DoS. یک کوئری تودرتو روی رابطه‌ای که خودش تودرتو است، می‌تواند از نظر ریاضی چند هزار رکورد را در یک درخواست بارگذاری کند.

graphql
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. 1تحلیل هزینه ایستا روی هر فیلد در زمان build، نه در زمان اجرا.
  2. 2توکین‌دهی به عمق و پیچیدگی کوئری، با سقف سخت که در صورت عبور، درخواست رد می‌شود.
  3. 3سقف تعداد رکورد در هر سطح (complexity limit per list field)، نه فقط در کل کوئری.

الگوی سوم: batch abuse و alias bomb

قابلیت `batch` به کلاینت اجازه می‌دهد چندین کوئری را در یک درخواست بفرستد. این برای صرفه‌جویی در رفت‌وبرگشت طراحی شده، اما در عمل یک ضربه‌کننده است: یک درخواست HTTP می‌تواند صدها کوئری مستقل را حمل کند و محدودیت نرخ سازمانی — که بر اساس تعداد درخواست HTTP تنظیم شده — عملاً بی‌اثر می‌شود.

الگوی چهارم: resolver های نوشته‌شده با ORM که query را دور می‌زنند

در عمل، شایع‌ترین ریشه یافته‌های GraphQL ما یک خطای امنیتی نیست — یک اشتباه معماری است. رزولوری که برای یک فیلد اضافی نوشته شده، گاهی یک `prefetch` اضافه می‌کند تا درخواست بعدی سریع‌تر باشد. آن prefetch با کلاس مدل ORM نوشته شده و فیلتر tenant را اعمال نمی‌کند — چون آن برای مسیر داخلی نوشته شده بود، نه مسیر عمومی.

این یافته‌ها در بازبینی کد بسیار آسان‌تر از آنچه در تست داینامیک دیده می‌شوند پیدا می‌شوند. به همین دلیل، در ارزیابی‌های GraphQL ما تست داینامیک همیشه با بازبینی سورس رزولورها همراه است — نه به‌عنوان جایگزین، بلکه به‌عنوان مکمل.

DevByte Research
تیم تست نفوذ و مهندسی معکوس