سلسلة Base64 غير صالحة: لماذا يفشل فك الترميز وكيف تصلحه
يرفض فاكّ ترميز Base64 السلسلة لواحد من ثلاثة أسباب: محرف خارج الأبجدية، أو طول لا يمكن أن يكون صحيحًا، أو حشو في غير موضعه.
يستخدم Base64 مجموعة من 64 محرفًا: من A إلى Z، ومن a إلى z، ومن 0 إلى 9، إضافة إلى + و/، مع = للحشو في النهاية (RFC 4648، القسم 4). وأي محرف آخر يُفشل فاكّ الترميز الصارم. والمحيّر أن فاكّات الترميز لا تتفق على ما يدخل تحت «أي محرف آخر»: سلسلة Base64URL مثل PDw_Pz8-Pg== تُفكّ بشكل صحيح مع Buffer في Node.js، وتفشل في .NET، وتتحوّل بصمت إلى بايتات خاطئة في Python. لذا فإن معرفة ما يتسامح معه فاكّ الترميز الذي تستخدمه هي معظم الحل.
الخطأ في كل لغة
| أين | ما الذي تحصل عليه |
|---|---|
Chrome وEdge، atob() | InvalidCharacterError: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded. |
Node.js، atob() | InvalidCharacterError: Invalid character، أو The string to be decoded is not correctly encoded. عند الطول الخاطئ |
Python، base64.b64decode() | binascii.Error: Incorrect padding، أو Invalid base64-encoded string: number of data characters (5) cannot be 1 more than a multiple of 4، أو مع validate=True: Only base64 data is allowed |
.NET، Convert.FromBase64String() | FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters. |
Java، Base64.getDecoder().decode() | IllegalArgumentException: Illegal base64 character متبوعة برمز المحرف بالنظام الست عشري، مثل 2d للمحرف - |
PHP، base64_decode($s, true) | لا رسالة: تعيد الدالة false |
| AWS CLI الإصدار 2 | Invalid base64: متبوعة بالقيمة التي مرّرتها |
أما Buffer.from(s, 'base64') في Node.js فهو الاستثناء بين هؤلاء: لا يرمي خطأ أبدًا. إنه يتخطّى المحارف التي لا يعرفها، فتنتج السلسلة التالفة بايتات تالفة دون أن يظهر أي خطأ.
ما يتسامح معه كل فاكّ ترميز
| المدخل | atob | Python b64decode | .NET | PHP في الوضع الصارم |
|---|---|---|---|---|
حشو ناقص: SGVsbG8 | يُفكّ | Incorrect padding | يفشل | يُفكّ |
حشو زائد: SGVsbG8== | يفشل | يُفكّ | يفشل | يعيد false |
| مسافات أو فواصل أسطر | تُتجاهل | تُتجاهل (ويفشل مع validate=True) | تُتجاهل | تُتجاهل |
محرفا Base64URL - و_ | يفشل | يُحذفان بصمت: بايتات خاطئة أو خطأ | يفشل | يعيد false |
محارف أخرى: علامات الاقتباس و\ و! و: | يفشل | تُحذف بصمت | يفشل | يعيد false |
الوضع الافتراضي في Python يستحق تحذيرًا خاصًا. فهو يتخلّص من كل محرف خارج الأبجدية قبل فك الترميز، فتُفكّ حروف بادئة مثل data:image/jpeg;base64, كأنها جزء من البيانات. وبحسب طول البادئة، تحصل إما على خطأ محيّر عن عدد المحارف، وإما على غياب تام للخطأ مع عدة بايتات من البيانات العشوائية أمام المحتوى الحقيقي. مرّر validate=True حين تريد أن تفشل المدخلات الخاطئة فشلًا واضحًا.
محارف لا تنتمي إلى السلسلة
حين يبلّغ فاكّ ترميز صارم عن محرف غير صالح، فالسلسلة في الغالب التقطت شيئًا ما في طريقها إليك:
- علامات اقتباس أو نص محيط. نسخ
"SGVsbG8="مع علامتي الاقتباس، أو نسخ سطر"key": "value"كاملًا، يضيف محارف ليست من Base64. - بادئة data URI. في
data:image/png;base64,iVBOR...لا يكون Base64 إلا الجزء الذي يلي الفاصلة. احذف كل ما قبلها، والفاصلة معه. - محارف الهروب في JSON. تكتب الدالة
json_encodeفي PHP المحرف/على شكل\/، والمحتوى الملفوف مثل شهادة PEM يُخزَّن مع\nبين الأسطر. فإن نسخت نص JSON الخام بدل القيمة المحلَّلة، جاءت الشرطات المائلة العكسية معه. حلّل JSON أولًا ثم فكّ ترميز النص الناتج. - محارف Base64URL. وجود
-و_يعني أن السلسلة تستخدم الأبجدية الآمنة للروابط، كما تفعل رموز JWT. راجع القسم الخاص بذلك أدناه. - محارف غير مرئية. المسافة غير القابلة للكسر أو المسافة صفرية العرض الملصوقة من تطبيق محادثة أو من مستند ليست مسافة ASCII عادية، لذا يرفضها حتى
atobالذي يتجاهل المسافات العادية.
وهناك حالة لا تُنتج أي خطأ على الإطلاق. إن وُضعت قيمة Base64 قياسية في سلسلة استعلام دون ترميز بالنسبة المئوية، تحوّلت كل + إلى مسافة عند تحليل الاستعلام. ثم يتخطّى atob وPython هذه المسافات: فتصل ++++YWJj على شكل YWJj وتُفكّ إلى abc، وقد ضاعت البايتات الثلاث الأولى بصمت. رمّز القيمة بـ encodeURIComponent، أو استخدم Base64URL في الروابط.
طول وحشو خاطئان
يحوّل Base64 كل 3 بايتات إلى 4 محارف، لذا فإن طول السلسلة المحشوّة يقبل القسمة على 4 دائمًا. وبدون حشو، تضم المجموعة الأخيرة محرفين أو 3 محارف. أما باقي القسمة 1 فمستحيل مع أي مدخل: إن كان length % 4 === 1 فقد ضاع محرف أو أُضيف محرف زائد، ولن يصلح ذلك أي قدر من الحشو.
لا يجوز أن يظهر الحشو إلا في النهاية، ولا تزيد علامات = عن اثنتين أبدًا. ويشترط RFC 4648 الحشو ما لم تنص الصيغة التي تستخدم Base64 على خلاف ذلك، وعدة صيغ تفعل ذلك: رموز JWT وتحديات الرمز في PKCE تحذفه. ولإعادة الحشو الناقص قبل تسليم السلسلة إلى فاكّ ترميز صارم:
// JavaScript
const padded = s + '='.repeat((4 - s.length % 4) % 4);
# Python
padded = s + '=' * (-len(s) % 4)
Base64URL في فاكّ ترميز قياسي
يستبدل Base64URL (RFC 4648، القسم 5) المحرف + بـ - والمحرف / بـ _، ويحذف الحشو عادةً. ويرفض فاكّ الترميز القياسي هذين المحرفين، لكن السلاسل التي تحتوي أحدهما مصادفةً هي وحدها التي تفشل، فيبدو الخلل عشوائيًا. فالقيمة العشوائية ذات 16 بايت تصبح 22 محرف Base64URL، يمكن أن يكون 21 منها - أو _. وكل محرف يتجنّب الاثنين باحتمال 62/64، لذا فإن نحو 51% من هذه القيم، أي (62/64) مرفوعًا للقوة 21، تُفكّ بلا مشكلة، وتفشل الـ 49% الباقية. الحل أن تحوّل الأبجدية وتعيد الحشو، أو أن تستخدم فاكّ ترميز مصمَّمًا لهذا النوع:
// Browser
const b64 = s.replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
// Node.js
Buffer.from(s, 'base64url');
# Python
base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))
// Java
Base64.getUrlDecoder().decode(s);
لا تزال urlsafe_b64decode في Python تحتاج إلى الحشو، ولهذا يضيفه المثال. وتجد الفروق بين الأبجديتين مشروحة بالتفصيل في دليل الفرق بين Base64 وBase64URL.
فواصل الأسطر في البريد وملفات PEM
تلفّ رسائل البريد بصيغة MIME نص Base64 عند 76 محرفًا في السطر، وتلفّه ملفات PEM مثل الشهادات عند 64 محرفًا. ويتخطّى atob وكذلك .NET وPHP فواصل الأسطر، أما فاكّ الترميز الأساسي في Java فلا يفعل: استخدم Base64.getMimeDecoder() للمدخلات الملفوفة. وفي ملفات PEM احذف أيضًا سطري -----BEGIN ...----- و-----END ...----- لأنهما ليسا من Base64.
نجح فك الترميز لكن النتيجة خاطئة
- الناتج Base64 آخر. رُمِّزت البيانات مرتين. فالسلسلة
YUdWc2JHOD0=تُفكّ إلىaGVsbG8=، وهذه تُفكّ إلىhello. فكّ الترميز مرة أخرى. - الحروف اللاتينية ذات العلامات تظهر على شكل
é. تعيدatobمحرفًا واحدًا لكل بايت، لذا يحتاج نص UTF-8 إلى خطوة ثانية:new TextDecoder().decode(Uint8Array.from(atob(s), c => c.charCodeAt(0))). - يرمي Python الخطأ
UnicodeDecodeErrorبعد فك الترميز. البايتات Base64 صالح لكنها ليست نص UTF-8، ومثال ذلك'utf-8' codec can't decode byte 0xe9لنص محفوظ بترميز Latin-1. فكّ الترميز بالترميز الصحيح، مثل.decode('cp1252')، أو تعامل مع النتيجة كبيانات ثنائية. - الناتج رموز غير مقروءة. البيانات ثنائية، كصورة أو ملف PDF أو بيانات مضغوطة. احفظ البايتات في ملف بدل طباعتها.
AWS CLI: الخطأ "Invalid base64"
يتوقع الإصدار 2 من AWS CLI أن تكون المعاملات الثنائية بصيغة Base64 افتراضيًا. لذا فإن تمرير JSON عادي إلى أمر مثل aws lambda invoke --payload '{"id": 1}' يفشل بالرسالة Invalid base64. أضف --cli-binary-format raw-in-base64-out لتمرير النص كما هو، أو رمّز الحمولة بنفسك.
فحص سلسلة بسرعة
الصق القيمة في أداة فك ترميز Base64. تزيل الأداة المسافات وبادئة data URI، وتحوّل محارف Base64URL، وتضيف الحشو الناقص قبل فك الترميز؛ فإن ظلّت تقول إن المدخل ليس Base64 صالحًا، فالسلسلة تحتوي فعلًا محرفًا دخيلًا أو طولها مستحيل. وحين ينجح فك الترميز، تعرض نص UTF-8 مباشرة، وتقدّم أي محتوى آخر، كالصور، ملفًا للتنزيل.