خطای Invalid Base64 String: چرا کدگشایی شکست می‌خورد و چطور رفعش کنیم

کدگشای Base64 به یکی از سه دلیل رشته را رد می‌کند: نویسه‌ای خارج از الفبا، طولی که ممکن نیست درست باشد، یا پدینگ در جای نادرست.

همین حالا امتحان کنید: رمزگشای Base64 کدگذار و کدگشای 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 نسخه 2Invalid 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 را مستقیم نشان می‌دهد و هر چیز دیگری، مثل تصویر، را به شکل فایل برای دانلود ارائه می‌کند.

سوالات متداول

پیام Incorrect padding در پایتون یعنی چه؟

طول رشته مضربی از ۴ نیست و پدینگ = وجود ندارد، که برای Base64URL و بخش‌های JWT عادی است. تا وقتی طول مضرب ۴ شود، = اضافه کنید. اگر طول بر ۴ باقی‌مانده ۱ دارد، مشکل پدینگ نیست و رشته آسیب دیده است.

چرا atob خطای InvalidCharacterError می‌دهد؟

رشته نویسه‌ای خارج از الفبای Base64 دارد، مثل - یا _ از Base64URL، کوتیشن یا بک‌اسلش، یا پدینگ اضافه یا طولی ناممکن دارد. فاصله‌ها و خط‌های جدید نادیده گرفته می‌شوند و علت خطا نیستند.

چرا یک رشته در یک زبان کدگشایی می‌شود و در زبان دیگر نه؟

کدگشاها سخت‌گیری متفاوتی دارند. پایتون و Buffer در Node.js به‌طور پیش‌فرض نویسه‌های ناشناخته را نادیده می‌گیرند، حالت strict در PHP و atob پدینگ ناقص را می‌پذیرند و دات‌نت هم پدینگ ناقص و هم نویسه‌های ناشناخته را رد می‌کند.

Base64URL را چطور کدگشایی کنم؟

نویسه - را با + و _ را با / جایگزین کنید، پدینگ = اضافه کنید تا طول مضربی از ۴ شود و بعد به روش معمول کدگشایی کنید. Node.js آن را مستقیم با Buffer.from(s, 'base64url') می‌پذیرد.
همین حالا امتحان کنید: رمزگشای Base64 کدگذار و کدگشای Base64