مرحباً بالجميع~
لقد وجدت خطأً غريباً نوعاً ما يتعلق بترميز b64encode مع قيم ‘+’ مما يسبب مشاكل مع نقطة النهاية sync_sso فقط. أنا واثق من أنه ليس مشكلة تكامل. لقد استخدمت Claude Fable لتشخيص الخطأ وإنشاء التقرير، آمل أن يكون هذا مقبولاً. لقد راجعت كل شيء، وأعدت إنتاج الخطأ بنفسي، وأوافق على السبب الجذري والحل. أخبرني إذا كنت بحاجة إلى أي شيء آخر. لم يعد الأمر حرجاً للغاية بالنسبة لي الآن لأنني وجدت حلاً بديلاً، لكنه حل غريب قد يواجهه آخرون.
شكراً لكم على كل عملكم الشاق!
براندون
ملخص
POST /admin/users/sync_sso يفك تشفير معلمة sso مرتين. إذا كان الحمولة المشفرة بـ base64 تحتوي على حرف +، فإن نقطة النهاية تفسدها إلى مسافة وترفض الطلب بالخطأ العام 422 Login Error — على الرغم من أن الحمولة والتوقيع صحيحان.
بالنسبة لمحتوى الحمولة (المشفر برمز ASCII)، ينتج base64 حرف + فقط عندما يكون حرف ~ (0x7E) في المحاذاة الصحيحة للبايت — وهذا فقط للعملاء الذين يتركون ~ غير محاصر في الترميز URL وفقاً لـ RFC 3986 (مثل Python ≥ 3.7، و encodeURIComponent في JavaScript).
لذا في الممارسة العملية، يظهر هذا كالتالي: المزامنة تعمل للجميع، باستثناء المستخدمين الذين يحتوي سيرهم الذاتي/اسم مستخدمهم/إلخ على ~، وأحياناً فقط، اعتماداً على كيفية تحول باقي ملفهم الشخصي للمحاذاة. يبدو وكأنه لغز HMAC خاص بالمستخدم ومن المستحيل تقريباً تشخيصه من الخارج. من المحتمل أن يؤثر هذا أكثر على منتديات موقعي من الآخرين، لأن الكوريين ومتعلمي اللغة الكورية يستخدمون ‘~’ بشكل متكرر لإنهاء السير الذاتية.
بالنسبة لتسجيلات الدخول والمزامنات عبر المتصفح، لا يظهر هذا الخطأ (انظر أدناه). إنه مشكلة فقط لمكالمات sync_sso من جانب الخادم.
السبب الجذري
في Admin::UsersController#sync_sso (users_controller.rb#L500):
sso = DiscourseConnect.parse("sso=#{params[:sso]}&sig=#{params[:sig]}", server_session:)
تم فك ترميز params[:sso] بالفعل بواسطة Rack/Rails، لكنه يُعاد إدراجه في بناء جملة سلسلة الاستعلام دون إعادة الترميز. ثم تقوم DiscourseConnect.parse بتشغيل Rack::Utils.parse_query عليه (discourse_connect_base.rb#L93) —
فك ترميز ثانٍ لقيمة تم ترميزها مرة واحدة فقط. يحول parse_query الحرف الحرفي + إلى مسافة، ويتحقق رمز base64 أدناه من ذلك ويرفع PayloadParseError،
يظهر كخطأ 422 “Login Error”. (قبل إضافة هذا التحقق في #26140، فشل نفس التلف في مقارنة التوقيع بدلاً من ذلك — نفس النتيجة.)
+ هو حرف أبجدية base64 الوحيد الذي يغيره فك ترميز النموذج، لذا فإن فك الترميز المزدوج هو عملية صامتة غير فعالة لمعظم الحمولات — وهو السبب في بقاء هذا الخطأ دون اكتشافه. لا تتأثر تدفقات تسجيل الدخول عبر المتصفح (session/sso_login): فهي تحلل سلسلة الاستعلام الخام، أي تفك الترميز مرة واحدة بالضبط.
يعود الاستيفاء إلى fb750af8e29a20bbbf4b70b0f3f4e4a06f602b69 (أكتوبر 2014،
“تحديث بسيط للسماح بنقطة نهاية API لـ sync_sso”)، الذي استبدل الأصل —
وصحيح — DiscourseSingleSignOn.parse(request.query_string).
هذا ليس خطأ ترميز من قبل مدمج: دليل sync_sso الرسمي
يوضح إرسال قيمة sso بتشفير نموذج قياسي واحد، وتفعل gem discourse_api الرسمية الشيء نفسه — لذا فإن العميل الذي يتبع التعليمات حرفياً يواجه هذا الخطأ كلما احتوت حمولة base64 على +.
خطوات إعادة الإنتاج (حتمية)
-
قم بإنشاء حمولة sync_sso موقعة صالحة لأي مستخدم، تتضمن حقلًا قيمته
تحتوي على~~~(ثلاثة علامات موجّهة متتالية تضمن هبوط واحدة في موضع بايت
≡ 2 mod 3، مما يجبر+في base64).ملاحظة لإعادة الإنتاج من Ruby: لا يمكن أن تحتوي الحمولات المبنية باستخدام فئة Discourse الخاصة
SingleSignOn(أو gemdiscourse_api) على+في base64 الخاص بها —Rack::Utils.build_queryيرمز~كـ%7E، ولا يوجد بايت في أبجدية إخراجها
يعادل+في base64. لإعادة الإنتاج، قم ببناء سلسلة الاستعلام الداخلية يدوياً
مع~~~حرفي في قيمة الحقل (يتم تجاهل المفاتيح غير المعروفة)، على سبيل المثال
Base64.strict_encode64("external_id=1&email=user@example.com&username=someuser&filler=~~~")،
قم بتوقيعه، وأرسل POST. هذا هو السبب أيضاً في أن المواصفات الحالية لم تلتقط الخطأ أبداً:
أدوات Discourse الخاصة لا يمكنها هيكلياً إنشاء حمولة محفزة. -
POST /admin/users/sync_ssoمعsso/sigكمعاملات نموذج مشفرة بشكل صحيح
(يتم إرسال+كـ%2Bعلى الشبكة). -
الاستجابة:
422 {"failed":"FAILED","message":"Login Error"}. -
نفس الحمولة مع استبدال العلامات الموجّهة بـ
xxx(base64 الآن خالٍ من+): 200.
تم التحقق منه ضد مثيل Discourse إنتاجي: بنية حمولة متطابقة، + واحد
في base64 → 422؛ صفر → 200؛ وتنجح حمولة 422 إذا تم ترميز + مسبقاً
كـ %2B (أي التعويض المسبق عن فك الترميز الإضافي).
كما تقوم مواصفة الانحدار الحالية بإرجاع الحمولة عبر
Rack::Utils.parse_query(sso.payload) قبل الإرسال — مما يفك ترميزها مسبقاً ويُلغي
فك الترميز الإضافي في المتحكم — لذا فإن أي حمولة تحتوي على + ستمر من خلالها.
الحل المقترح
أعد ترميز المعلمة قبل إعادة بناء سلسلة الاستعلام، محاكياً ما تفعله
جانب موفر SSO بالفعل باستخدام CGI.escape(payload) في نفس الفئة
(discourse_connect_base.rb#L168):
sso = DiscourseConnect.parse(
"sso=#{CGI.escape(params[:sso])}&sig=#{params[:sig]}", server_session:
)
حل بديل للمدمجين (حتى يتم الإصلاح)
قم بترميز + مسبقاً كـ %2B في قيمة sso لمكالمات sync_sso فقط (وليس لإعادة التوجيه لتسجيل الدخول عبر المتصفح). لاحظ أنه يجب إزالة هذا الحل البديل إذا/عندما يتم إصلاح نقطة النهاية، لأنه سيؤدي بعد ذلك إلى الإفراط في الترميز.