سلسلة 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 الإصدار 2Invalid base64: متبوعة بالقيمة التي مرّرتها

أما Buffer.from(s, 'base64') في Node.js فهو الاستثناء بين هؤلاء: لا يرمي خطأ أبدًا. إنه يتخطّى المحارف التي لا يعرفها، فتنتج السلسلة التالفة بايتات تالفة دون أن يظهر أي خطأ.

ما يتسامح معه كل فاكّ ترميز

المدخلatobPython b64decode.NETPHP في الوضع الصارم
حشو ناقص: 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 مباشرة، وتقدّم أي محتوى آخر، كالصور، ملفًا للتنزيل.

الأسئلة الشائعة

ماذا تعني رسالة "Incorrect padding" في Python؟

طول السلسلة ليس من مضاعفات 4 وحشو = ناقص، وهذا أمر طبيعي في Base64URL وأجزاء JWT. أضف = حتى يصبح الطول من مضاعفات 4. أما إن ترك الطول باقيًا قدره 1 عند القسمة على 4، فالسلسلة تالفة.

لماذا يرمي atob الخطأ InvalidCharacterError؟

تحتوي السلسلة محرفًا خارج أبجدية Base64، مثل - أو _ من Base64URL، أو علامة اقتباس، أو شرطة مائلة عكسية، أو أن فيها حشوًا زائدًا أو طولًا مستحيلًا. المسافات وفواصل الأسطر تُتجاهل وليست السبب.

لماذا تُفكّ السلسلة نفسها في لغة وتفشل في أخرى؟

تختلف فاكّات الترميز في صرامتها. يتخطّى Python وBuffer في Node.js المحارف المجهولة افتراضيًا، ويقبل الوضع الصارم في PHP وكذلك atob الحشو الناقص، بينما يرفض .NET الحشو الناقص والمحارف المجهولة معًا.

كيف أفكّ ترميز Base64URL؟

استبدل - بـ + و_ بـ /، وأضف حشو = حتى يصبح الطول من مضاعفات 4، ثم فكّ الترميز كالمعتاد. ويقبله Node.js مباشرة عبر Buffer.from(s, 'base64url').