
PoC — تعيد الطلبات عبر الأصول إعادة استخدام مفتاح API الخاص بالمزوّد المُهيّأ في inference-gateway (GHSA-5293-fcm6-fh8v، CVE-2026-87009، CVSS 5.4).
| الباحث | Dostxodjayev Abdullox (@squeeze440) |
| التنبيه | GHSA-5293-fcm6-fh8v |
| CVE | CVE-2026-87009 |
| CVSS 3.1 | 5.4 (متوسط) — CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L |
| الضعف | CWE-352, CWE-306, CWE-346 |
| الحالة | تم الإصلاح في v0.46.0 |
الملخص: يقوم inference-gateway بالارتباط بـ 0.0.0.0 ويأتي مع تعطيل المصادقة (AUTH_ENABLED=false) افتراضيًا، كما أن مسار التمرير ANY /proxy/:provider/*path يقوم بشكل غير مشروط بإزالة أي ترويسة Authorization يقدمها المستدعي واستبدالها بمفتاح API الخاص بالمزود المُهيأ على الخادم من قبل مشغل البوابة قبل التمرير إلى المنبع، دون أي سياسة CORS ودون أي حماية CSRF من أي نوع، مما يسمح لأي صفحة ويب يزورها متصفح الضحية بتوجيه طلبات LLM المفوترة بصمت عبر حساب OpenAI/Anthropic/إلخ الخاص بالضحية نفسه.
المنتج: inference-gateway/inference-gateway — بوابة LLM ذاتية الاستضافة وسحابية المنشأ (Go, Gin).
الإصدار المُختبر: commit 6677da6afd0a606899f833c2351635edeef386f5 (main, 2026-08-04). المتأثر: <= 0.45.0.
ثلاث حقائق مستقلة تتحد لتشكل الخلل.
1. المصادقة معطلة والارتباط عام افتراضيًا.
config/config.go:77 — AuthConfig.Enabled افتراضيًا false. config/config.go:94 — ServerConfig.Host افتراضيًا 0.0.0.0. مع تعطيل المصادقة، تُرجع NewOIDCAuthenticatorMiddleware (api/middlewares/auth.go:27-30) قيمة OIDCAuthenticatorNoop، التي تكون دالتها Middleware() (api/middlewares/auth.go:48-52) مجرد تمرير خالص. لا يوجد فحص هوية لكل طلب على أي مسار في هذا الوضع. ملف البدء السريع examples/docker-compose/basic/docker-compose.yml ينشر 8080:8080 دون تعيين AUTH_ENABLED، لذا فإن مسار البدء الموثق ينتج هذه التهيئة بالضبط.
2. /proxy/:provider/*path يحقن دائمًا مفتاح المزود الخاص بالمشغل.
api/routes.go:102-131 (ProxyHandler) يستدعي applyProviderAuth، api/routes.go:287-312:
func applyProviderAuth(req *http.Request, provider core.IProvider) error {
req.Header.Del("Authorization") // caller's own Authorization header is discarded
token := provider.GetToken() // the operator's configured key (env var, e.g. OPENAI_API_KEY)
switch provider.GetAuthType() {
case constants.AuthTypeBearer:
req.Header.Set("Authorization", "Bearer "+token)
...
لا يوجد مسار برمجي يُستخدم فيه اعتماد المستدعي نفسه؛ التصميم يستبدل دائمًا المفتاح المُهيأ للبوابة. مع اتحاده بالحقيقة 1، يحصل مستدعٍ غير مُصادق على مفتاح المشغل الحقيقي مُرفقًا مجانًا.
3. لا توجد سياسة CORS ولا حماية CSRF في أي مكان في سلسلة الوسائط.
cmd/gateway/main.go:271 يبني الموجّه باستخدام gin.New() (بدون وسائط افتراضية)؛ السلسلة (:273-290) هي otel → logger → telemetry → OIDC auth → guardrails → MCP. لا يحتوي go.mod/go.sum على أي حزمة CORS. لا تُرسل ترويسة Access-Control-Allow-Origin أبدًا. لا يحتاج معالج البروكسي إلى ترويسة مخصصة أو Content-Type غير آمن لـ CORS ليعمل (فهو يمرر الجسم الخام، ثم يستبدل Content-Type الصادر بـ application/json عند api/routes.go:254)، لذا فإن fetch() عبر الأصول "البسيط" (Content-Type: text/plain، بدون ترويسات مخصصة) يُرسل من المتصفح دون preflight. يكتمل الطلب المفوتر من جانب الخادم بشكل مستقل عن تطبيق CORS من جانب القراءة في المتصفح.
الأثر الصافي: أي أصل يزوره متصفح الضحية، بينما تكون البوابة قابلة للوصول من ذلك المتصفح (loopback، أو LAN، أو عام إذا اتبع المشغل نمط النشر الموثق 8080:8080)، يمكنه توجيه إكمالات محادثة عشوائية يختارها المهاجم عبر حساب المزود الحقيقي للمشغل، دون أي مصادقة ودون أي مؤشر مرئي للمستخدم.
تم التأكيد ديناميكيًا من البداية إلى النهاية باستخدام متصفح Chrome حقيقي يقوم بطلب حقيقي عبر الأصول بين أصلين مختلفين على loopback (البوابة على 127.0.0.1، صفحة المهاجم على 127.0.0.2). انظر poc/:
poc/attacker_site/attack.html — الصفحة الدقيقة المقدمة من أصل المهاجم؛ إجراؤها الوحيد عند التحميل هو fetch() واحد إلى /proxy/openai/chat/completions.poc/mock_upstream.py — يحل محل api.openai.com، ويسجل ترويسة Authorization وOrigin والجسم الذي يستقبله.poc/README.md — خطوات التشغيل الكاملة.لوحظ: الطلب عبر الأصول من المتصفح (Origin: http://127.0.0.2:8000) وصل إلى المنبع الوهمي حاملًا Authorization: Bearer sk-proj-VICTIM-REAL-BILLED-KEY-... والجسم الذي اختاره المهاجم {"messages":[{"role":"user","content":"CSRF-DRIVEBY-MARKER-8271"}]} — دون أن تمتلك صفحة المهاجم أي اعتماد أو تراه أو يُطلب منها. أكد فحص شبكة المتصفح أن POST http://127.0.0.1:8081/proxy/openai/chat/completions [200] انطلق من صفحة 127.0.0.2:8000. الأدلة الكاملة المدفوعة بالمتصفح (سجل الشبكة، لقطة الشاشة) مرفقة بـ GHSA-5293-fcm6-fh8v.
أي مشغل يشغل inference-gateway بتهيئته الافتراضية الموثقة تصبح مفاتيح API الخاصة بالمزود المُهيأة لديه قابلة للاستخدام من قبل أي صفحة ويب يمكن الوصول إليها من متصفح يستطيع الوصول إلى منفذ البوابة، دون أي اعتماد أو ملف تعريف ارتباط أو موقع شبكي خاص يتجاوز "القدرة على إرسال طلب HTTP إلى عنوان البوابة". بشكل ملموس: استهلاك غير مصرح به للفواتير/الحصة على حساب المزود الخاص بالمشغل، مدفوعًا بشكل أعمى من أي موقع طرف ثالث أو إعلان أو صفحة مخترقة يفتحها المشغل (أو أي شخص على نفس شبكة LAN) أثناء تشغيل البوابة. يحجب المتصفح المهاجم من قراءة مخرجات النموذج (لا توجد ترويسات CORS)، لذا فهذه معاملة قسرية عمياء، وليست قدرة قراءة.
Origin/Sec-Fetch-Site، ودون قيد CORS./health لديه صفر فحص هوية لكل طلب عندما يكون AUTH_ENABLED=false، وهو الافتراضي الموثق.تم الإصلاح في v0.46.0 (قام المشرف بتقوية الإعدادات الافتراضية). التدابير الموصى بها:
/proxy/:provider/*path (والمسارات الأخرى التي تغير الحالة)، مما يفرض preflight CORS على المستدعين عبر الأصول ويمنح البوابة مكانًا لفرض قائمة أصول مسموح بها. هذا يغلق تجاوز "الطلب البسيط" دون الحاجة إلى تمكين المصادقة.SERVER_HOST من 0.0.0.0 إلى 127.0.0.1، مع اشتراط موافقة صريحة على واجهة أوسع (كما فعل Ollama لنفس فئة الخلل).AUTH_ENABLED=false وSERVER_HOST ليس loopback.README.md / Configurations.md.Dostxodjayev Abdullox (@squeeze440)