Приходится иметь дело с форматом Base64? Тогда этот сайт идеально вам подойдет! Воспользуйтесь нашим невероятно удобным онлайн-инструментом для кодирования или декодирования ваших данных.

Декодирование Base64 в PHP: полное руководство

Она появляется в тикете поддержки, в логе API, в конфигурационном файле или посреди URL: длинная строка из букв и цифр, изредка + или /, а в конце, возможно, один-два знака =. Вы узнаёте её мгновенно. Base64 - это формат из двоичного в текст: он переписывает каждые три байта сырых данных в четыре символа, взятых из 64-буквенного алфавита, а когда количество байтов не кратно трём, хвост дописывают пара-другая знаков =. Декодирование - это сжимающее направление этой сделки: на входе четыре символа, на выходе три байта. Домашняя страница этого сайта пошагово разбирает формат, поэтому статья тратит силы туда, куда положено: на PHP-сторону работы.

Сначала главная новость. PHP держит декодер Base64 в своём ядре ещё с PHP 4. Функции base64_decode() не нужны ни расширение, ни пакет Composer, ни какая-либо конфигурация, и она работает везде, где работает PHP. Новость похуже: её настроение по умолчанию молча глотает испорченный ввод и отдаёт вам мусор, не сказав ни слова. Хорошая новость становится ещё лучше: один флаг ($strict) превращает функцию в настоящего смотрителя, а когда вы поймёте, как выбрать настроение, доказать, что вход настоящие данные, и превратить байты обратно в смысл, Base64 перестанет быть источником загадочных багов и станет рутинной задачей, которую можно автоматизировать.

Короткое замечание о размере: декодирование сжимает данные примерно на четверть (на каждые четыре входных символа выходит три байта), поэтому результат всегда занимает меньше памяти, чем вход. Вам никогда не придётся бояться, что декодирование раздуется. Так что знакомьтесь: это наш инструмент.

Функция, которая делает всю работу

Вот полная сигнатура, в точности как её сообщает современный PHP:

base64_decode(string $string, bool $strict = false): string|false

Три слова в этой строке делают всю работу. У $string нет ограничения по размеру: мегабайт декодируется заметно быстрее миллисекунды, так что ничто не мешает вам декодировать целый файл одним вызовом. Тип возврата формулирует весь контракт: либо строка декодированных байтов, либо false. Исключений нет, кодов ошибок нет, второго канала нет. false - это единственный сигнал, который вы получаете, так что проверка на него - часть работы. И одно предложение из мануала заслуживает того, чтобы его выучить наизусть: возвращаемые данные могут быть двоичными. В ту же секунду, как в результате оказываются PNG, ZIP или хеш, это уже не «строка текста» в каком бы то ни было широком смысле, и PHP с удовольствием позволит вам и дальше обращаться с ней как с текстом. Эта гибкость - и суперсила, и ловушка, а разделы ниже держат её под контролем.

Быстрый проход по версионным меткам, потому что унаследованный код любит строить допущения. Функция находится в ядре с PHP 4. Её параметр $strict появился в PHP 5.2.0, в ноябре 2006 года. Начиная с PHP 8.0, сигнатура несёт настоящие нативные типы (те самые string и bool, что выше, плюс возвращаемое string|false), так что IDE и статические анализаторы наконец знают: функция может завершиться ошибкой. Начиная с PHP 8.1, передача null вызывает уведомление об устаревании; если вы имеете в виду «ничего», явно пишите '':

$decoded = base64_decode('');
var_dump($decoded); // string(0) ""

Строгий режим или тихая уборка

Флаг $strict - это переключатель между двумя очень разными характерами. Выключен (по умолчанию) - и декодер становится дружелюбным забывалой: каждый символ вне алфавита Base64 молча отбрасывается, остальное декодируется, и никому об этом не сообщают. Мануал формулирует это прямо: иначе недопустимые символы будут молча отброшены. Включён - и декодер становится смотрителем: первый нераспознанный символ обречает весь входной блок на false.

Вот сводка нанесённого урона. Каждая строка ниже - это реальное поведение base64_decode() на PHP 8.x:

Вход Лояльный (по умолчанию) Строгий
Zm9vYmFy, чистая "foobar" "foobar"
Zm9v\r\nYmFy, CRLF в середине строки "foobar" "foobar"
" Zm9vYmFy ", пробелы по краям "foobar" "foobar"
Zm9v\x0bYmFy, вертикальная табуляция "foobar" false
Zm9v\x00YmFy, встроенный байт NUL "foobar" false
V@hpcy, лишний @ 3 байта мусора false
Zm9vY, пять символов "foo", последний символ отброшен false
Z, одна буква "", пустая строка false
=Zm9, заполнения в начале "fo" false
Zm9vYmFy==, заполнения после полной группы "foobar" false
Zm9vYmFy==A, данные после заполнений "foobar" false
Zm9vYmF, семь символов, без заполнений "fooba" "fooba"

Три строки заслуживают второго взгляда. Строка V@hpcy показывает, почему лояльный режим опасен везде, где вход не заслуживает доверия: лишний @ не останавливает декодирование, он просто исчезает, и три вышедших байта ничего не значат. Строка с одиночной Z показывает, что пустой результат почти ничего не доказывает: односимвольный блок «декодируется» в пустую строку, не вызывая ошибки. Строка Zm9vYmFy==A показывает, что декодер с удовольствием игнорирует данные, появившиеся после заполнений, - именно так усечённый или подделанный блок может выглядеть безупречно.

Что строгий режим всё же пропускает? Ровно четыре пробельных символа: пробел, табуляция, возврат каретки и перевод строки, в любом месте, даже сразу рядом со знаками =. Это осознанный выбор. Почтовые блоки, обёрнутые по MIME, несут CRLF-переводы строк внутри закодированного потока, и строгий режим проглатывает их без какой-либо предобработки (почему - объяснит раздел о почте ниже). Всё остальное, что не буква алфавита, от байтов NUL до вертикальных табуляций, обречено на false.

Есть одна настоящая уступка, о которой стоит знать, хотя это не особенность PHP: PHP молча достраивает недостающие заполнения за вас. Семисимвольный блок Zm9vYmF (без заполнений вовсе) декодируется в "fooba" точно так же, как его дополненный тезка Zm9vYmF=, и в том, и в другом настроении. RFC 4648 требует заполнения в общем случае, так что принятие хвоста без заполнений - это осознанное послабление, и оно не специфично для PHP: RawStdEncoding в Go и декодер Java принимают тот же ввод без заполнений. Если ваша PHP-сторона и сторонняя система не сходятся в оценке пограничного блока, сначала стоит поискать недостающее заполнение.

Стандарт поддерживает строгое настроение. В RFC 4648, раздел 3.3, сказано, что реализации должны отклонять закодированные данные, содержащие символы вне алфавита, если окружающая спецификация не говорит иного (MIME - классический случай «говорить иного»). Тот же раздел объясняет, почему: символы вне алфавита можно использовать как скрытый канал, пряча информацию в символах, которые ваш декодер отбрасывает, и их уже использовали, чтобы вызывать ошибки в декодерах. Если ваш вход приходит из внешнего мира, строгий режим - не вопрос стиля. Это то, чего требует стандарт.

Доказываем, что блок - это Base64

Декодер, который умеет проваливаться молча, заслуживает валидационного конвейера перед собой. Три уровня, каждый из которых ловит то, что пропускают остальные.

Первый уровень - проверка формы с помощью регулярного выражения: только символы алфавита и не более двух заполнений в самом конце.

$shapeLooksPlausible = preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $payload) === 1;

Регулярное выражение ловит очевидный мусор (лишние пробелы, знаки @, заполнение посреди строки) до того, как что-либо запустится. Но валидатором оно не является: оно не видит, что Zm9vYmFy= - это девять символов с одним заполнением, и строгий режим тоже в этом откажет. Именно поэтому существует второй уровень. Строгое декодирование - единственная проверка, которая понимает семантику Base64, поэтому последнее слово за ним.

Третий уровень забывают все: явно обрабатывайте false, потому что это единственный сигнал, который вы получаете.

function decode_payload(string $payload): string
{
  $clean = str_replace(["\r", "\n"], '', $payload);
  $decoded = base64_decode($clean, true);
  if ($decoded === false) {
    throw new InvalidArgumentException('Not a valid Base64 payload.');
  }
  return $decoded;
}

Начальный str_replace() - необязательный комфорт: строгий режим и так терпит CRLF, но его удаление сохраняет чистоту любых расчётов длины, которые вы будете делать позже, потому что число символов в чистом блоке всегда кратно четырём. (На один символ больше кратного четырёх, как пять или девять, в Base64 невозможно, и строгий режим это отвергнет.) Обратите внимание: функция сама по себе никогда не бросает исключение; проверку писать придётся вам.

Base64, безопасная для URL

В дикой природе вы встретите второй алфавит, и именно он кусается. Стандартный Base64 использует + и /, две буквы, которые в URL приносят беду: + в строке запроса превращается в пробел ещё до того, как PHP успевает его увидеть, а / - это разделитель пути. RFC 4648, раздел 5, определяет исправление: алфавит, безопасный для URL и имён файлов, где + становится -, / становится _, а хвостовые заполнения = обычно убирают, чтобы сэкономить символы. RFC непреклонен: это «не следует считать тем же самым, что и кодирование base64», и самое частое, что вы услышите, - название base64url. JSON Web Tokens, параметры state в OAuth, идентификаторы сессий API и адреса видеохостингов - всё это живёт в этом диалекте.

Сторона декодера - это два шага: вернуть алфавит обратно, затем восстановить недостающие заполнения. Вот помощник, который в итоге приживётся у вас везде:

function base64url_decode(string $data): string|false
{
  $standard = strtr($data, '-_', '+/');
  $missing = strlen($standard) % 4;
  if ($missing !== 0) {
    $standard .= str_repeat('=', 4 - $missing);
  }
  return base64_decode($standard, true);
}
var_dump(base64url_decode('aGk_PnRoZXJl')); // string(9) "hi?>there"

Современный PHP здесь на вашей стороне: он сам достраивает недостающие заполнения, так что явное восстановление - это двойная страховка (и оно сохраняет переносимость кода на старые версии PHP). Направление опасности - одностороннее. Если подать URL-безопасный текст в стандартный декодер в лояльном режиме, символы - и _ просто не входят в стандартный алфавит, а значит, будут отброшены. Ваш результат окажется короче, чем должен, без ошибки, без уведомления, без ничего. Сначала делайте замену через strtr(), а ещё лучше - всегда проходите через помощника.

Честная оговорка: если URL-безопасный блок случайно не содержит ни -, ни _, то два алфавита побайтово совпадают для этих конкретных данных, и не имеет значения, каким декодером вы воспользовались. Опасность проявляется только тогда, когда эти символы присутствуют, потому что именно там алфавиты различаются.

Текст, байты и кодировки

Base64 не имеет понятия о том, что значат ваши байты, и декодер PHP наследует эту слепоту. Кодер слеп к кодировкам: он возвращает те же самые 8-битные значения, что вошли, будь то текст UTF-8, текст Windows-1252, JPEG или хеш. Сам PHP на той же волне: строка - это последовательность байтов и ничего больше. Как только вы хотите отобразить результат или сравнить его с другим текстом, кто-то должен ответить на два вопроса: это вообще текст, и если да, то в какой кодировке?

Практический тест имеет два ведра. Двоичные данные почти всегда выдают себя байтами NUL и нижними управляющими байтами, а текст, не являющийся валидным UTF-8, - это второе ведро. Расширение mbstring (по умолчанию не включено) даёт вам строгую проверку UTF-8:

function looks_binary(string $bytes): bool
{
  if ($bytes === '') {
    return false;
  }
  if (strpbrk($bytes, "\x00\x01\x02\x03\x04") !== false) {
    return true;
  }
  return !mb_check_encoding($bytes, 'UTF-8');
}
var_dump(looks_binary("\x89PNG\r\n\x1a\n...png body")); // bool(true)
var_dump(looks_binary("héllo wörld, 日本語"));          // bool(false)

Когда блок - это текст в устаревшей кодировке, перекодируйте его до того, как он коснётся вашего HTML. Windows-1252 - самая распространённая устаревшая кодировка для веба и настольных данных, и разница между ней и простым ISO-8859-1 решает, будет ли байт 0x93 фигурной кавычкой или невидимым управляющим символом:

// «café» в Windows-1252: буква é - это один байт, 0xE9
$legacy = base64_decode('Y2Fm6Q==', true);
$utf8 = mb_convert_encoding($legacy, 'UTF-8', 'Windows-1252');
var_dump($utf8); // string(5) "café": теперь é - это два байта UTF-8

Предупреждение о знаменитом mb_detect_encoding(): сам PHP-мануал говорит, что автоматическое определение «никогда не может быть полностью надёжным», и сравнивает его с расшифровкой сообщения без ключа. Покормите его Windows-1252 «café» - и он, возможно, скажет Windows-1252; покормите его заголовком PNG - и он с удовольствием снова скажет Windows-1252, потому что семейство кодировок ISO-8859 определено для любого возможного значения байта и поэтому может совпасть с чем угодно. Считайте определение последним средством, доверяйте объявленной кодировке (заголовок, строка конфигурации, сортировка базы данных), когда она существует, а остальное по умолчанию отправляйте в UTF-8 или в разряд двоичных данных.

Когда блок - это файл

Самая распространённая файловая задача - обратная к той, что сделал какой-то экспортирующий код: приходит текстовый файл .b64, и вам нужен исходный файл обратно. Со строгим декодированием и проверкой на false это уже похоже на код для продакшена:

$encoded = file_get_contents('/var/www/uploads/blob.b64');
$decoded = base64_decode($encoded, true);
if ($decoded === false) {
  http_response_code(400);
  exit('That upload is not valid Base64.');
}

Строки PHP - это просто байты, поэтому ничего на этом пути не заботится о том, что представляет собой блок: текстовый файл, ZIP-архив или видео. Математика размера работает на вас: декодированный результат на три четверти короче закодированного входа, поэтому декодирование никогда не ухудшает положение с памятью.

Хорошая привычка - позволить байтам объявить о себе до того, как вы начнёте доверять любому ярлыку. Класс finfo (расширение fileinfo, входит в стандартные сборки PHP) говорит вам, что данные представляют собой на самом деле:

$mime = (new finfo(FILEINFO_MIME_TYPE))->buffer($decoded);
var_dump($mime); // string(9) "image/png"
$extensions = ['image/png' => 'png', 'application/pdf' => 'pdf', 'application/zip' => 'zip'];
$ext = $extensions[$mime] ?? 'bin';
$target = '/var/www/uploads/file-' . bin2hex(random_bytes(4)) . '.' . $ext;
file_put_contents($target, $decoded);

Последний шаг важнее, чем кажется. Блок, который выдаёт себя за изображение, но декодируется во что-то другое, - это ровно тот случай, который ловит вторая пара глаз. И если вы позже отдадите восстановленный файл браузеру, Content-Type, который вы отправите, должен приходить из той же проверки finfo, а не из имени файла.

Data URI, формат из буфера обмена

Любимый вид прибывающих: кто-то вставляет изображение в форму, и фронтенд передаёт вам полноценный data URI: data:image/png;base64,iVBORw0KGgo.... RFC 2397 определяет форму: data:, опциональный MIME-тип, опциональный флаг ;base64, запятая, а затем данные. Если флаг присутствует, блок - это Base64; если отсутствует, блок - это percent-кодированный обычный текст, редкий, но законный вариант. Если MIME-тип опущен, по умолчанию действует text/plain;charset=US-ASCII. Зачем здесь вообще Base64? Потому что URI не может безопасно содержать сырые байты или запятые, а Base64 даёт вам один алфавит, которому не нужны никакие экранирования.

function split_data_uri(string $uri): ?array
{
  if (!str_starts_with($uri, 'data:') || !str_contains($uri, ',')) {
    return null;
  }
  $meta = substr($uri, 5, strpos($uri, ',') - 5);
  $payload = substr($uri, strpos($uri, ',') + 1);
  $isBase64 = str_ends_with($meta, ';base64');
  $mime = $isBase64 ? substr($meta, 0, -7) : $meta;
  if ($mime === '') {
    $mime = 'text/plain;charset=US-ASCII';
  }
  return [$mime, $isBase64, $payload];
}
$uri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ';
[$mime, $isBase64, $payload] = split_data_uri($uri);
var_dump($mime); // string(9) "image/png"

В этом формате живёт две ловушки. Первая - отсутствующий флаг ;base64: законный data URI без флага несёт percent-кодированный блок, и прогон его через base64_decode() даст мусор. Вторая - заявленный MIME-тип: это подсказка от отправителя, а не факт. Проверка finfo из раздела о файлах - вот ваш факт. И помните собственный совет RFC: data URI полезны только для коротких значений; изображение в несколько мегабайт внутри URL - это запах, а не образец для подражания.

JWT: токены, в которые можно заглянуть

Самый известный Base64-блок в вебе - это JSON Web Token, и самый нестрашный, как только вы узнали его форму. Согласно RFC 7519, компактный JWT - это три части URL-безопасного Base64, разделённые точками: заголовок, блок данных и подпись, каждая закодирована без заполнений и без переводов строк (RFC 7515 прямо говорит, что лишних символов не должно проскочить). Заголовок и блок данных - это обычный JSON, поэтому их может прочитать каждый, и поэтому каждый должен понять следующий абзац, прежде чем трогать токен.

Чтение двух первых частей занимает пять строк с помощником из примера выше, и это отличный способ снять с токена мистику:

$token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.8GljXWCrvkTYln_WtTVhyWSzflOC1iGL8jBDUHQmaEE';
[$headerPart, $payloadPart] = explode('.', $token);
$header  = json_decode(base64url_decode($headerPart), true);
$payload = json_decode(base64url_decode($payloadPart), true);
var_dump($header);
// array(2) { ["alg"] => string(5) "HS256" ["typ"] => string(3) "JWT" }
var_dump($payload);
// array(3) { ["sub"] => string(10) "1234567890" ["name"] => string(8) "John Doe" ["iat"] => int(1516239022) }

Теперь важная часть: третья часть - это подпись, а две части, которые вы только что декодировали, не секретны и не аутентифицированы. Их может прочитать любой, у кого есть захват пакетов, и переписать - любой, у кого есть текстовый редактор. Доверять блоку данных до проверки подписи - это классический JWT-баг. Для продакшена не пишите эту проверку вручную. Ответ сообщества - пакет firebase/php-jwt, в текущей версии v7, соответствующий RFC 7519 и требующий PHP 8.0 или новее. Установите его через Composer:

composer require firebase/php-jwt

После этого API сначала проверяет подпись и отдаёт вам блок данных только если она подтвердилась:

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$secret = 'correct-horse-battery-staple-long-enough-secret';
try {
  $claims = JWT::decode($token, new Key($secret, 'HS256'));
  var_dump($claims->sub); // свойство, и только после того, как подпись подтвердилась
} catch (UnexpectedValueException $e) {
  // некорректный токен, плохая подпись или просроченные claims
}

Одна заметка о версиях: v7 библиотеки требует минимальной длины ключа для алгоритмов HMAC, так что секрет HS256 короче 32 байт будет отклонён через DomainException ещё до проверки подписи. Держите секреты длинными; библиотека не даст вам об этом забыть.

Следите за порядком в этом API: JWT::decode() бросает исключение при плохой подписи, просроченном токене или отсутствующем алгоритме, а не возвращает мусор, поэтому блок данных, который вы получаете обратно, можно доверять. Ручная версия выше - для понимания и для того, чтобы заглянуть в токены, которые не были для вас предназначены; а библиотека - для доверия.

HTTP Basic Auth, самый старый заголовок

Самый старый заголовок аутентификации в вебе всё ещё едет на Base64. Согласно RFC 7617, запрос HTTP Basic отправляет Authorization: Basic, а за ним - Base64-кодирование строки username:password. RFC прямо говорит, что это кодирование, а не защита: любой, у кого есть захват пакетов, декодирует обе половины одним нажатием клавиши. Ваша работа на стороне декодирования - разобрать заголовок, декодировать строго и сравнить с помощью функции, устойчивой к timing-атакам.

function basic_credentials(string $header): ?array
{
  if (!str_starts_with($header, 'Basic ')) {
    return null;
  }
  $decoded = base64_decode(substr($header, 6), true);
  if ($decoded === false || !str_contains($decoded, ':')) {
    return null;
  }
  [$user, $password] = explode(':', $decoded, 2);
  return [$user, $password];
}
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$creds = basic_credentials($header);
if ($creds !== null
  && hash_equals('alice', $creds[0])
  && hash_equals('secret123', $creds[1])
) {
  // аутентифицирован
}

Две детали делают это безопасным. Лимит 2 в explode() важен, потому что пароль законно может содержать двоеточия, а сравнение должно быть через hash_equals(), никогда через ==, чтобы атакующий не прошёл по вашему списку пользователей по таймингам. И отдавайте это только через HTTPS; на обычном соединении слой Base64 - всего лишь декор.

Электронная почта, где всё началось

Base64 родился из конкретной проблемы: почтовая пересылка перевозила только 7-битный ASCII, а люди хотели отправлять двоичные файлы. Стандарт MIME (RFC 2045, раздел 6.8) сделал Base64 одной из кодировок передачи двоичных данных и добавил два внутренних правила. Первое: закодированные строки не должны превышать 76 символов. Второе: программное обеспечение для декодирования должно игнорировать каждый символ вне алфавита, включая переводы строк. Именно второе правило объясняет, почему декодер PHP, в любом настроении, проглатывает CRLF-обёрнутый блок без какой-либо предобработки с вашей стороны. (Отсюда же и терпимость к \r\n, которую вы видели в таблице строгого режима выше.)

$png = "\x89PNG\r\n\x1a\n" . random_bytes(256);
$wrapped = chunk_split(base64_encode($png), 76, "\r\n");
// позже, на принимающей стороне, никаких очисток не нужно:
$decoded = base64_decode($wrapped, true);
var_dump($decoded === $png); // bool(true): каждый байт прошёл полный круг

Две практические заметки. Первая: обёртка добавляет вес: при CRLF каждые 76 символов вложение в 100 КБ прибудет примерно как 137 КБ текста (обычный коэффициент четыре-третьих плюс накладные расходы на переводы строк). Вторая: для настоящей почты с заголовками, несколькими частями и quoted-printable соседями необязательное расширение mailparse разбирает полные сообщения RFC 822 часть за частью; для одного известного вложения достаточно строгого декодирования.

PEM-броня: ключи и сертификаты

Сертификаты и ключи путешествуют в PEM-броне: метка BEGIN, блок Base64 строками по 64 символа и метка END. Длина строки в 64 символа - это конвенция, унаследованная от оригинальной спецификации Privacy Enhanced Mail (RFC 1421), инструменты OpenSSL её ожидают, так что она важна, когда вы восстанавливаете обёртку заново. Когда вы декодируете, она вообще не важна: декодер просто игнорирует переводы строк.

$pem = file_get_contents('/etc/ssl/my-key.pem');
preg_match('/-----BEGIN ([A-Z ]+)-----\s*(.*?)\s*-----END \1-----/s', $pem, $m);
$label = $m[1];
$der = base64_decode(preg_replace('/\s+/', '', $m[2]), true);
if ($der === false) {
  // всё-таки не Base64
}
var_dump($label); // string(11) "PRIVATE KEY"

Декодированные байты - это DER, компактная двоичная сериализация, и именно с ней в конечном счёте работают функции openssl_*. Обратная ссылка \1 в регулярном выражении - тихий герой: она гарантирует, что метка END совпадает с меткой BEGIN, и именно так вы избегаете пришивания END сертификата к BEGIN ключа, когда в файле лежит несколько блоков.

Потоки и большие блоки

Декодирование - направление, которое вам помогает: результат на три четверти меньше входа, поэтому давление на память из-за Base64 - редкость. Тем не менее, когда на диск приземляется файл .b64 на сотни мегабайт, у вас есть два инструмента, чтобы держать потребление памяти плоским.

Первый - поблочное декодирование. Разделите очищенный вход на куски, длина которых кратна четырём символам, строго декодируйте каждый кусок и соедините. Каждый блок - самодостаточный валидный блок данных, поэтому на границах ничего не теряется, а испорченный файл быстро обнаруживается, с точным смещением, которое можно показать.

$clean = str_replace(["\r", "\n"], '', file_get_contents('/var/www/uploads/huge.b64'));
$decoded = '';
$chunkSize = 4 * 50000; // кратно четырём символам, примерно 150 КБ на выходе за вызов
for ($offset = 0; $offset < strlen($clean); $offset += $chunkSize) {
  $part = base64_decode(substr($clean, $offset, $chunkSize), true);
  if ($part === false) {
    exit('Corrupted payload near offset ' . $offset);
  }
  $decoded .= $part;
}

Мегабайт Base64 декодируется на современном железе заметно быстрее миллисекунды, поэтому этот цикл почти ничего не стоит; выбирайте его за свойства валидации и отчётности, а не за скорость.

Второй инструмент - гражданин поточного мира: потоковый фильтр convert.base64-decode. Он работает с любым потоком PHP, так что вы можете декодировать прямо из файлового указателя, php://input или memory-потока, ни разу не держа весь закодированный текст в одной переменной. Как и лояльная функция, он просто пропускает каждый символ вне алфавита Base64:

$in = fopen('/var/www/uploads/huge.b64', 'rb');
$out = fopen('/var/www/uploads/huge.bin', 'wb');
stream_filter_append($in, 'convert.base64-decode', STREAM_FILTER_READ);
stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);

Какой инструмент выбрать? Фильтр - когда данные текут через поток и вы хотите, чтобы PHP сам занимался коммуникациями; циклический блок - когда нужна поблочная валидация, отчёт о прогрессе или смещение повреждения.

Базы данных, конфигурационные файлы и переменные окружения

Base64 - это текстовый контейнер, и поэтому он появляется в местах, где его не ждёшь. В базах данных двоичный blob (файл, иконка, сериализованная структура) может жить в столбце TEXT в виде Base64, переживая любой инструмент, который предполагает текст. Ожидайте, что хранимое значение будет примерно на 33 процента больше оригинала, и выбирайте размеры столбцов соответственно. В конфигурационных файлах и переменных окружения Base64 - это способ пронести значения, которые иначе сломают формат: DSN базы данных с точками с запятой, пароль с кавычками, значение с переводом строки.

// .env или конфиг, написанные ops-специалистом:
//   DB_DSN_B64 = cGc6aG9zdD1kYjtwYXNzd29yZD1xdSJvdGU=
$dsn = base64_decode(getenv('DB_DSN_B64') ?: '', true);
if ($dsn === false) {
  exit('DB_DSN_B64 is not valid Base64.');
}
// теперь $dsn: pg:host=db;password=qu"ote

Та же осторожность действует здесь дважды. Во-первых, это безопасность формата, а не секретность: как только разработчик прочитает конфигурационный файл, он сможет декодировать значение одним вызовом. Никогда не храните секрет в виде Base64, называя это шифрованием. Во-вторых, валидируйте при старте: испорченное или наполовину вставленное значение env - это false от строгого вызова, и однострочная проверка превращает загадочную ошибку времени выполнения в понятное сообщение при запуске.

Из командной строки

Не всё декодирование происходит внутри веб-запроса. CLI-скрипты, cron-задачи и однострочники декодируют Base64 всё время, и командная строка - место, где функция встречается с php://stdin:

php -r 'fwrite(STDOUT, base64_decode(file_get_contents("php://stdin"), true));' < payload.b64 > restored.bin

У shell уже есть собственная Base64-утилита (coreutils base64 -d), и она годится для быстрых дел; PHP-однострочник нужен, когда следующий шаг - PHP-логика: запись в базу данных, вызов API, запуск валидации. Две ловушки, специфичные для shell. Вывод декодирования - сырые байты, поэтому отправляйте его в файл или в команду, которая понимает байты, а не в терминал, который их испортит. И держите строгий флаг включённым в однострочнике, потому что усечённая вставка в терминале заслуживает false, а не трёх байтов мусора.

Ловушки с PHP-акцентом

Короткий тур по ловушкам, специфичным для PHP, всё в одном месте:

  • Главная - лояльный режим по умолчанию. base64_decode('V@hpcy') возвращает три байта мусора без предупреждения, поэтому каждый декодер недоверенного входа нуждается в строгом флаге и проверке на false.
  • Одиночный символ в лояльном режиме декодируется в пустую строку, и строка из одних пробелов тоже. Пустой результат почти ничего не доказывает; только false означает сбой, и получите вы его лишь в строгом режиме.
  • + в строке запроса уже является пробелом до того, как PHP его видит. Если клиент отправляет ?token=abc+def без percent-кодирования, PHP передаст вам abc def (это поведение form-кодирования, общее для parse_str() и urldecode()), и никакое декодирующее волшебство не вернёт плюс. URL-безопасный Base64 (без плюсов вовсе) - исправление для токенов в URL.
  • Недостающие заполнения достраиваются за вас, молча. Семь символов декодируются как восемь; это удобно, но означает, что блок, усечённый на одно-два заполнения, всё равно декодируется без замечаний, поэтому чистое декодирование никогда полностью не доказывает, что блок прибыл целым (raw-кодировщики Go и Java так же снисходительны).
  • Призрак mbstring.func_overload. Долго устаревшая настройка, переписывавшая strlen() и подобных на подсчёт символов (удалена в PHP 8.0), раньше портила байтовую арифметику Base64 на UTF-8 строках. Унаследованный код может до сих пор нести комментарии и обходные пути для неё. Удалите их.
  • Декодированные байты - не строка UTF-8. Запуск preg_match() с флагом /u или mb_substr() над декодированными двоичными данными - мгновенный источник ошибок «некорректный вход». Сначала понюхайте, потом решайте.
  • Передача null устарела с PHP 8.1. Если переменная может быть null, схлопните её в '' перед вызовом.
  • $_GET и товарищи декодируются по правилам форм, а не по правилам URL. Если значение пришло percent-кодированным, rawurldecode() - более безопасная обратная операция, потому что она не трогает +.

Краткая история base64_decode

Сам Base64 старше большей части современного веба (управляющий им стандарт, RFC 4648, датирован 2006 годом, а он кодифицировал MIME-кодирование 1996 года, которое, в свою очередь, ведёт происхождение от PEM-брони начала 1990-х). История PHP - это её собственный небольшой changelog.

PHP 4 поставил base64_decode() как ядерную функцию без опций и без строгого режима; лояльное настроение было единственным настроением, и не было никакого способа попросить декодер пожаловаться. PHP 5.2.0, в ноябре 2006 года, добавил флаг $strict, и запись в changelog стоит прочесть: он был добавлен, чтобы обеспечить соответствие RFC 3548, предшественнику сегодняшнего RFC 4648. Оказалось, что этот единственный флаг - самое полезное добавление за всю жизнь функции.

Потом пришли годы отладки. PHP 5.3 исправил серию багов строгого режима в двух точечных релизах: баг #52327 (начальные заполнения обрабатывались неправильно в строгом режиме, исправлено в 5.3.4) и баг #55273 (пробельные символы после заполнений отвергались в строгом режиме, исправлено в 5.3.9). (Исправление целочисленного переполнения 2016 года тоже числится под именем этой функции: баг #72836, официально озаглавленный «целочисленное переполнение в base64_decode вызывает повреждение кучи» и исправленный в 5.6.25, но собственный код воспроизведения из отчёта о баге и патченный показывают, что переполнение было на самом деле в расчёте длины base64_encode(), а не в декодере; название - неточность, унаследованная от исходного отчёта.) Каждое исправление подтягивало поведение, которое вы видите в таблице выше. PHP 8.0 дал обеим Base64-функциям нативные типы параметров и возврата, ту самую сигнатуру, что вы видели в начале статьи, и та же линия релизов убрала mbstring.func_overload, настройку, которая годами молча портила байтовую арифметику. PHP 8.1 объявил устаревшим передачу null в них. С тех пор поверхность заморожена: один параметр, один флаг, один тип возврата, без изменений.

Несколько радостей для нёрдов

Раз уж это длинный референс, вот несколько фактов, специфичных для PHP, которые просто приятны:

  • Пустая идентичность. base64_encode('') и base64_decode('') - оба ''. Функции обращаются с пустотой как с первоклассным значением в обоих направлениях, без всякого false.
  • Странный адрес. В PHP-мануале обе Base64-функции живут в главе «URLs» книги «Прочие базовые расширения». Отдельной главы «кодирование» нет; вот там вы их и найдёте, вверху списка той главы, перед parse_url() и его друзьями.
  • Декодер - это гомоморфизм. Классическая заметка пользователя на php.net отмечает, что функция является гомоморфизмом между строками, разбитыми на сегменты по модулю 4, и строками, разбитыми по модулю 3, а это формальный способ сказать, что любое разбиение на куски, кратные четырём, - валидное разбиение. Именно поэтому раздел о поблочном декодировании вообще работает, и именно поэтому файл в 1 МБ можно декодировать кусками по 50 КБ без потерь.
  • Один параметр, один флаг. За более чем двадцать лет base64_decode() получил ровно один параметр ($strict), а base64_encode() - ни одного.
  • У него есть старшие братья. То же ядровое расширение несёт в себе и convert_uuencode(), и convert_uudecode() (в мануале они числятся под String Functions, функции строк), реликты эпохи dial-up, когда uuencode был основным транспортным средством для двоичных данных. Вам они почти никогда не понадобятся, но если в ваш ящик когда-нибудь прилетит древний файл .uu, PHP сможет его открыть.
  • Строгий режим держит открытую дверь для почты. Четыре пробельных символа (пробел, табуляция, возврат каретки и перевод строки) специально проходят сквозь строгий режим, поэтому MIME-обёрнутому вложению не нужна предобработка. Всё остальное, включая байты NUL, - это false.

Обратное направление

Вот и вся сторона декодера, и именно здесь живёт большая часть боли, потому что декодирование - это место, где вы встречаете чужие данные: их выбор заполнений, их переводы строк, их кодировки, их токены. Обратное направление, превращение байтов в Base64-строку через base64_encode(), - животное спокойнее: оно никогда не падает, у него нет строгого режима, а его собственный набор ловушек (двойное кодирование, несовпадение обёрток, счёт за размер) заслуживает отдельного гайда. Кодирование Base64 в PHP, на которое есть ссылка с этой страницы, разбирает кодировщик с той же глубиной.

Последнее обновление: 2026-09-08

Связанная статья: Кодирование Base64 в PHP: полное руководство