خطای Invalid Base64 String: چرا کدگشایی شکست میخورد و چطور رفعش کنیم
کدگشای Base64 به یکی از سه دلیل رشته را رد میکند: نویسهای خارج از الفبا، طولی که ممکن نیست درست باشد، یا پدینگ در جای نادرست.
Base64 از ۶۴ نویسه استفاده میکند: A تا Z، a تا z، 0 تا 9، + و /، بهعلاوه = که بهعنوان پدینگ در انتهای رشته میآید (RFC 4648، بخش ۴). هر چیز دیگری باعث میشود کدگشای سختگیر شکست بخورد. بخش گیجکننده ماجرا این است که کدگشاها درباره اینکه «هر چیز دیگری» دقیقاً شامل چه میشود همنظر نیستند: رشته Base64URL مثل PDw_Pz8-Pg== با Buffer در Node.js درست کدگشایی میشود، در داتنت شکست میخورد و در پایتون بیصدا به بایتهای اشتباه تبدیل میشود. دانستن اینکه کدگشای شما چه چیزهایی را تحمل میکند، بیشتر راه رفع خطاست.
پیام خطا در هر زبان
| کجا | چه چیزی میگیرید |
|---|---|
کروم و اج، 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. |
پایتون، 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 |
داتنت، 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. |
جاوا، 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 | پایتون b64decode | داتنت | PHP در حالت strict |
|---|---|---|---|---|
پدینگ ناقص: SGVsbG8 | کدگشایی میکند | Incorrect padding | شکست | کدگشایی میکند |
پدینگ اضافه: SGVsbG8== | شکست | کدگشایی میکند | شکست | false برمیگرداند |
| فاصله یا خط جدید | نادیده میگیرد | نادیده میگیرد (با validate=True شکست) | نادیده میگیرد | نادیده میگیرد |
نویسههای - و _ در Base64URL | شکست | بیصدا حذف میشوند: بایتهای اشتباه یا خطا | شکست | false برمیگرداند |
نویسههای دیگر: کوتیشن، \، !، : | شکست | بیصدا حذف میشوند | شکست | false برمیگرداند |
حالت پیشفرض پایتون جای هشدار دارد. این حالت پیش از کدگشایی، هر نویسهای را که خارج از الفباست دور میریزد؛ پس حروف پیشوندی مثل data:image/jpeg;base64, طوری کدگشایی میشوند که انگار جزو دادهاند. بسته به طول پیشوند، یا خطایی گیجکننده درباره تعداد نویسهها میگیرید، یا هیچ خطایی نمیگیرید و چند بایت زباله جلوی محتوای واقعی مینشیند. هر وقت میخواهید ورودی نادرست بهروشنی شکست بخورد، validate=True را بدهید.
نویسههایی که جایشان اینجا نیست
وقتی کدگشای سختگیر از نویسه نامعتبر خبر میدهد، رشته تقریباً همیشه در مسیر رسیدن به شما چیزی به خودش اضافه کرده است:
- کوتیشن یا متن اطراف. کپی کردن
"SGVsbG8="همراه با کوتیشنهایش، یا یک خط کامل"key": "value"، نویسههایی اضافه میکند که جزو Base64 نیستند. - پیشوند data URI. در
data:image/png;base64,iVBOR...فقط بخش بعد از کاما Base64 است. همه چیز را تا خود کاما حذف کنید. - Escapeهای JSON. تابع
json_encodeدر PHP نویسه/را به شکل\/مینویسد و محتوای چندخطی، مثل گواهی PEM، با\nمیان خطها ذخیره میشود. اگر بهجای مقدار پارسشده، متن خام JSON را کپی کنید، بکاسلشها هم همراهش میآیند. اول JSON را پارس کنید و بعد رشته حاصل را کدگشایی کنید. - نویسههای Base64URL. وجود
-و_یعنی رشته از الفبای امن برای URL استفاده میکند، همان کاری که JWTها میکنند. بخش مربوط را در ادامه ببینید. - نویسههای نامرئی. فاصله نشکن (non-breaking space) یا فاصله با عرض صفر که از یک پیامرسان یا سند چسبانده شده، فاصله سفید ASCII به حساب نمیآید؛ برای همین حتی
atobکه فاصلههای معمولی را نادیده میگیرد، آن را رد میکند.
یک حالت اصلاً خطا تولید نمیکند. اگر یک مقدار Base64 استاندارد بدون کدگذاری درصدی در رشته کوئری URL قرار بگیرد، هنگام پارس شدن کوئری هر + به فاصله تبدیل میشود. بعد atob و پایتون این فاصلهها را نادیده میگیرند: ++++YWJj به شکل YWJj میرسد و به abc کدگشایی میشود، در حالی که سه بایت اول بیصدا از دست رفتهاند. مقدار را با encodeURIComponent کدگذاری کنید یا در URLها از Base64URL استفاده کنید.
طول و پدینگ نادرست
Base64 هر ۳ بایت را به ۴ نویسه تبدیل میکند، پس طول رشتهای که پدینگ دارد همیشه بر ۴ بخشپذیر است. بدون پدینگ، گروه آخر ۲ یا ۳ نویسه دارد. باقیمانده ۱ برای هیچ ورودیای ممکن نیست: اگر length % 4 === 1 باشد، نویسهای از دست رفته یا نویسهای اضافه شده است و هیچ مقدار پدینگی آن را درست نمیکند.
پدینگ فقط در انتها میتواند بیاید و هیچوقت بیشتر از دو علامت = نیست. RFC 4648 پدینگ را الزامی میداند، مگر اینکه قالبی که از Base64 استفاده میکند چیز دیگری بگوید؛ و چند قالب دقیقاً همین را گفتهاند: JWTها و code challengeهای PKCE پدینگ را حذف میکنند. برای برگرداندن پدینگ جاافتاده، پیش از دادن رشته به یک کدگشای سختگیر:
// JavaScript
const padded = s + '='.repeat((4 - s.length % 4) % 4);
# Python
padded = s + '=' * (-len(s) % 4)
Base64URL در کدگشای استاندارد
Base64URL (RFC 4648، بخش ۵) بهجای + از - و بهجای / از _ استفاده میکند و معمولاً پدینگ را هم کنار میگذارد. کدگشای استاندارد هر دو نویسه را رد میکند، اما فقط رشتههایی شکست میخورند که اتفاقاً یکی از آنها را داشته باشند؛ برای همین باگ تصادفی به نظر میرسد. یک مقدار تصادفی ۱۶ بایتی به ۲۲ نویسه Base64URL تبدیل میشود که ۲۱ تای آنها میتوانند - یا _ باشند. هر نویسه با احتمال ۶۲/۶۴ هیچکدام از این دو نیست، پس (۶۲/۶۴) به توان ۲۱، یعنی حدود ۵۱ درصد از این مقدارها، درست کدگشایی میشوند و ۴۹ درصد دیگر شکست میخورند. الفبا را تبدیل و پدینگ را بازسازی کنید، یا از کدگشایی استفاده کنید که برای همین گونه ساخته شده است:
// 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 در پایتون همچنان به پدینگ نیاز دارد و به همین دلیل مثال بالا آن را اضافه میکند. تفاوتهای دو الفبا در راهنمای تفاوت Base64 و Base64URL بررسی شده است.
خط جدید در ایمیل و فایلهای PEM
بدنه ایمیلهای MIME، متن Base64 را در خطهای ۷۶ نویسهای میشکند و فایلهای PEM، مثل گواهیها، در خطهای ۶۴ نویسهای. atob، داتنت و PHP از خطهای جدید میگذرند، اما کدگشای پایه جاوا این کار را نمیکند: برای ورودی چندخطی از Base64.getMimeDecoder() استفاده کنید. در PEM، خطهای -----BEGIN ...----- و -----END ...----- را هم حذف کنید، چون Base64 نیستند.
کدگشایی شد، اما نتیجه اشتباه است
- خروجی خودش Base64 است. داده دو بار کدگذاری شده است.
YUdWc2JHOD0=بهaGVsbG8=کدگشایی میشود و آن هم بهhello. یک بار دیگر کدگشایی کنید. - حروف دارای علامت مثل é به شکل
éدرآمدهاند.atobبرای هر بایت یک نویسه برمیگرداند، پس متن UTF-8 به یک مرحله دیگر نیاز دارد:new TextDecoder().decode(Uint8Array.from(atob(s), c => c.charCodeAt(0))). - پایتون بعد از کدگشایی
UnicodeDecodeErrorمیدهد. بایتها Base64 معتبرند، اما متن UTF-8 نیستند؛ مثلاً'utf-8' codec can't decode byte 0xe9برای متنی که با Latin-1 ذخیره شده است. با کدگذاری درست، مثل.decode('cp1252')، کدگشایی کنید یا نتیجه را داده باینری در نظر بگیرید. - خروجی نمادهای ناخواناست. داده باینری است، مثل تصویر، PDF یا داده فشرده. بهجای چاپ کردن بایتها، آنها را در یک فایل ذخیره کنید.
پیام Invalid base64 در AWS CLI
AWS CLI نسخه 2 بهطور پیشفرض انتظار دارد پارامترهای باینری به شکل Base64 باشند. دادن JSON ساده به فرمانی مثل aws lambda invoke --payload '{"id": 1}' با پیام Invalid base64 شکست میخورد. گزینه --cli-binary-format raw-in-base64-out را اضافه کنید تا متن همانطور که هست فرستاده شود، یا payload را خودتان کدگذاری کنید.
بررسی سریع یک رشته
مقدار را در کدگشای Base64 بچسبانید. این ابزار پیش از کدگشایی، فاصلههای سفید و پیشوند data URI را حذف میکند، نویسههای Base64URL را تبدیل میکند و پدینگ جاافتاده را اضافه میکند؛ پس اگر باز هم بگوید ورودی Base64 معتبر نیست، رشته واقعاً نویسهای بیگانه دارد یا طولش ناممکن است. وقتی کدگشایی موفق باشد، متن UTF-8 را مستقیم نشان میدهد و هر چیز دیگری، مثل تصویر، را به شکل فایل برای دانلود ارائه میکند.