Base64 Decoding in Perl: A Complete Guide
You have been handed a string of letters, digits, and the occasional + or /, and you know deep down that it is not what it looks like. Maybe it is a token riding in an Authorization header, a .b64 file dug out of a support ticket, a certificate wearing its -----BEGIN armor, or a blob sitting quietly in a config file. You open a terminal, you type perl, and one question takes over everything else: how do I get the real data back?
The answer is small and reassuring. Perl has shipped a Base64 module with the language itself since 2002, and one function call, decode_base64, does the entire job: nothing to install, nothing to configure. A quick refresher while the coffee brews: Base64 rewrites every three bytes of data as four characters from a 64 symbol alphabet, padding the tail with one or two = signs so the result always lands on a multiple of four, which is why the encoded form typically runs about 33 percent larger than what it started from. The main page of this site explains the format in full, so this guide spends all of its time on the Perl side of the fence: the decoder's rules, the dialects, and the real world formats you will actually meet.
The Toolkit: Five Functions, Zero Installations
Every call you need lives in MIME::Base64, which has been part of the core Perl distribution since 5.8, so it is present on every serious installation, from the one embedded in router firmware to the one on a database server. The check is one line:
perl -MMIME::Base64 -e 'print $MIME::Base64::VERSION, "\n"'
# 3.16_01
Here is the decoding side of the module, the whole of it:
| Function | What it does | Notes |
|---|---|---|
decode_base64($str) |
the star of this article: turns a Base64 blob into raw bytes | silently ignores every non alphabet character, forever |
MIME::Base64::decode($str) |
the same decoder, called without an import | the form you will meet in plenty of older scripts |
decode_base64url($str) |
decodes the URL safe dialect with - and _, with or without padding |
added in 3.11 in 2010; the one that reads JWTs |
MIME::Base64::decoded_base64_length($str) |
tells you how big the decoded data will be, without decoding | not exported by default, handy for pre sizing buffers |
unpack("u", $data) |
decodes uuencoded data, the pre Base64 format | built into Perl itself, no module required |
The version map for those functions, in case you are maintaining a fleet of old boxes:
| Feature | Available since |
|---|---|
decode_base64() with the C speed path |
Perl 5.8 in 2002, when the module joined the core |
decoded_base64_length() |
module 3.10 in 2010 |
decode_base64url() |
module 3.11 in 2010 |
| Quiet decoding, no warnings on suspect input | module 3.11 in 2010 |
| The current 3.16 line | 2020, requires Perl 5.6.2 or newer |
If your system Perl is missing the module for some reason, and it should not be, the fix is one of two lines: the distro package libmime-base64-perl on Debian and Ubuntu, or cpanm MIME::Base64 to pull the current release from CPAN, where the module has lived as a dual life package since its core days. For the odd machine without a C compiler, the pure Perl twin MIME::Base64::Perl on CPAN provides the same basic interface, a few times slower but good enough for anything but bulk work. That is the entire dependency story: nothing else.
A Decoder That Never Says No
The contract is one line long. Hand it a string, and it hands you back the decoded bytes as an ordinary Perl string holding raw octets. No objects, no exceptions, no flags. The documentation states the two rules that define its personality in a single sentence: any character not part of the 65 character Base64 subset is silently ignored, and any character occurring after a = padding character is never decoded. That politeness is the most important thing in this article, so let it work for you once:
use MIME::Base64 qw(decode_base64);
print decode_base64("TWFu!"), "\n"; # Man - the bang vanishes without a trace
print decode_base64("TWFu=XX"), "\n"; # Man - everything after = is skipped
print decode_base64("TQ"), "\n"; # M - no warning, no comment
print decode_base64("T"), "\n"; # the empty string, still no complaint
The last two lines are the leniency at its extreme. TQ carries one full byte plus four spare bits, and the decoder simply keeps the byte and drops the remainder. T carries not even one full byte, so the result is empty. There is no strict mode and no validator in the module to restore old school fussiness: since version 3.11 in 2010, decode_base64 does not even warn about truncated input, and older versions used to carp a Premature end of base64 data warning under -w. If the blob is wrong, it decodes anyway, which makes you the quality gate.
Here is the leniency policy in one place, so you can see the whole of it at a glance:
| Input | Result | Why |
|---|---|---|
"TWFu" |
Man |
clean input, the happy path |
"TWFu!" |
Man |
the bang is not in the alphabet, so it is skipped |
"TWFu=XX" |
Man |
nothing after the padding is ever decoded |
"TWFuIFdvcmxkIQ==" |
Man World! |
whitespace anywhere is free |
"TQ" |
M |
one full byte fits, the spare bits are dropped quietly |
"T" |
the empty string | not even one full byte, and no warning either |
"ab-cd_efgh" |
silently wrong bytes | the URL safe letters are dropped as noise, the classic trap |
That final row is the one to remember. A base64url segment handed to the standard decoder does not fail: it decodes to plausible looking garbage, because the - and _ characters are treated as foreign noise while the remaining letters still form valid groups. The decoder is a witness, not a gatekeeper, so if the input is untrusted, you validate it yourself. A small strict check is all it takes:
sub strict_base64 {
my ($blob) = @_;
$blob =~ s/[\r\n]//g; # the decoder ignores these, so do we
return 0 unless length($blob) % 4 == 0;
return $blob =~ /\A[0-9A-Za-z+\/]+(?:={1,2})?\z/ ? 1 : 0;
}
print strict_base64("TWFu"), "\n"; # 1
print strict_base64("TQ="), "\n"; # 0 - bad padding count
print strict_base64("ab-cd"), "\n"; # 0 - URL safe alphabet
One small word about that regex, from a hard won lesson: if a sub ends in a bare return $x =~ /.../ and the failed match result is fed straight into printf, Perl raises a misleading Missing argument in printf warning instead of a clean zero. Coerce the match with ? 1 : 0 before returning, as the function above does, and the trick disappears entirely.
Bytes First, Characters Second
Remember what decode_base64 returns: raw bytes, an ordinary string with no UTF 8 flag set. What those bytes mean is a decision only you can make, and it is the step where Unicode trips people up. Perl tracks whether a string holds characters or bytes, and length(), substr(), and most of the regular expressions behave differently depending on the answer. The fix is to name your encoding on purpose, with the Encode module that ships with every Perl install:
use MIME::Base64 qw(decode_base64);
use Encode qw(decode);
my $raw = decode_base64("SMOrbGxvIFdvcmxkIQ==");
my $text = decode("UTF-8", $raw);
print $text, "\n"; # Hëllo World!
print length($text), " chars\n"; # 12
print length($raw), " bytes\n"; # 13
That pair of numbers is the whole lesson. The blob is 13 bytes but only 12 characters, because the accented letter takes two bytes in UTF 8. Skip the charset step and the bytes will still print fine to a UTF 8 terminal, which is exactly why the mistake stays hidden until a string function counts them, or the bytes travel through a pipeline that expects characters. When in doubt, decode with a strict charset and let the exception tell you the truth about the bytes: decode() dies on invalid sequences instead of guessing, which is a feature.
The short list of charsets you will actually reach for:
| Charset | When to use it | Watch out for |
|---|---|---|
UTF-8 |
the default assumption: APIs, JSON, web content, modern text | invalid sequences die cleanly, which is exactly what you want |
Latin-1 |
legacy Western text, one byte per character, can never fail | it will happily mangle UTF 8 into double encoded mojibake |
ASCII |
data you are certain is plain 7 bit text | any byte above 127 dies |
UTF-16 |
Windows text, where the byte order mark decides endianness | the BOM is the only endianness hint, so keep it in the bytes |
And here is the trap the charset step protects you from. If the bytes you decoded are already UTF 8 and you run them through encode("UTF-8", ...) on the way out, you do not get a copy: you get a double encoding, where every accented character balloons into two characters of its own. The classic symptom is text that used to read Hëllo and now reads Hëllo, and the receiver on the other side of the wire will decode that faithfully. Bytes in, bytes out, one named conversion in between.
base64url: The Alphabet for URLs and Tokens
Half of the Base64 that crosses the modern internet is not the standard alphabet at all. The + character is how a browser encodes a space in a query string, and / is a path separator, so the standard letters are a disaster in URLs. RFC 4648, section 5, defines the fix: a second alphabet that swaps + and / for - and _, and by convention drops the = padding and the line breaks as well. The RFC is explicit that this encoding should not be regarded as the same as the base64 encoding, and Perl has had a dedicated pair for it since version 3.11 in 2010:
use MIME::Base64 qw(decode_base64url);
my $raw = decode_base64url("c3Vuc2V0LTQy");
print $raw, "\n"; # sunset-42
Two things to know. First, decode_base64url is happy with unpadded input, which is the form you will actually find in the wild, so the restore-the-padding-first ritual that other languages require does not apply here; padded input works too. Second, the standard decoder is a different animal: feed it a base64url segment and you get silently wrong bytes, because the - and _ characters are dropped as noise and the rest still decodes. Use the right decoder, or normalize by hand when you are stuck on a legacy code path:
my $seg = "ab-cd_efgh";
$seg =~ tr{-_}{+/}; # the URL safe letters, translated home
$seg .= "=" x (-length($seg) % 4); # padding restored for the standard decoder
my $raw = decode_base64($seg);
You will meet base64url immediately in JWTs, the tokens that every modern API hands out, and in any opaque ID that lives in a URL: eleven character video IDs, UUIDs stored in the URL safe alphabet (CPAN has Data::UUID::Base64URLSafe for exactly this), and database keys that need to survive an address bar. And if you are on an old Perl that predates the core functions, the standalone MIME::Base64::URLSafe module from 2006, a port of Python's urlsafe codec, provides urlsafe_b64encode and urlsafe_b64decode; on anything from 3.11 onward, the built ins are the better choice.
JWTs: Reading the Header and the Payload
A JSON Web Token is, structurally, two pieces of JSON wearing a disguise plus a cryptographic receipt. The compact form from RFC 7515 is three base64url segments joined by dots: the protected header, the payload, and the signature. Splitting and reading one is three lines:
use MIME::Base64 qw(decode_base64url);
use JSON::PP;
my $jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJob21lciJ9.uzM6l0c4...";
my ($head_b64, $claims_b64, $sig_b64) = split /\./, $jwt, 3;
my $head = decode_json(decode_base64url($head_b64));
my $claims = decode_json(decode_base64url($claims_b64));
print $claims->{sub}, " (", $head->{alg}, ")\n"; # homer (HS256)
Note the division of labor: decode_base64url turns each segment into bytes, and decode_json from the core JSON::PP module, present since Perl 5.14, turns the header and payload bytes into Perl data structures. The signature segment is base64url too, but it is a cryptographic digest, so you only ever decode the first two segments and let a proper library handle the third.
The pitfall is the one everyone forgets: readable does not mean valid. The header and the payload are readable by design, which also means anyone can rewrite them; the signature is the only proof. For anything real, verify, do not just decode. The CPAN module Crypt::JWT, which builds on CryptX, does the whole job:
use Crypt::JWT qw(decode_jwt);
my $claims = decode_jwt(
token => $jwt,
key => $secret,
accepted_alg => "HS256",
);
It croaks on a bad signature, and pinning accepted_alg closes the algorithm confusion hole where an attacker flips the token to a weaker variant. Decode and print is fine for inspecting a token during a support call; it is not authentication.
Files: From the .b64 Back to the Original
Files are where Perl's one liner culture really shines, and the whole job fits in one command. The -0777 flag is the secret ingredient, because it slurps the entire file into a single string instead of feeding the decoder line by line:
perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' < in.b64 > out
The line by line form is safe for one specific class of files: those where every line holds a multiple of four Base64 characters, which is true of every properly MIME wrapped body, since 76 is a multiple of 4. The moment the wrap points go ragged, and hand wrapped files usually are, the line by line decode starts producing padding in the middle of the data. Slurp mode has no such condition, which is why it is the default choice:
perl -MMIME::Base64 -ne 'print decode_base64($_)' < in.b64 > out
Inside a script, the pattern is the standard Perl file dance, with one quiet but important detail: the :raw layers on both handles, so Perl never tries to interpret the bytes as platform text on the way in or out:
use MIME::Base64 qw(decode_base64);
use Digest::SHA qw(sha256_hex);
open my $in, "<:raw", $ARGV[0] or die $!;
local $/;
my $blob = <$in>;
close $in;
my $decoded = decode_base64($blob);
print sha256_hex($decoded), "\n"; # compare with the sender's checksum
open my $out, ">:raw", $ARGV[1] or die $!;
print {$out} $decoded;
close $out;
The hash line is doing more than showing off. Because the decoder will accept almost anything, a matching checksum against what the sender published is the only proof that the trip was byte exact. For truly gigantic files, the line by line loop is the low memory alternative, provided the wraps sit on four character boundaries, and MIME::Base64::decoded_base64_length tells you how big the output will be before you commit to a buffer.
PEM Armor: Strip the Coat, Keep the DER
The .pem files in every security stack are the same Base64 wearing armor: a header line, a footer line, and a body wrapped at 64 characters per the old Privacy Enhanced Mail convention. The wrapper is the only interesting part, because the module's decoder does not care about line lengths at all:
use MIME::Base64 qw(decode_base64);
open my $fh, "<:raw", "cert.pem" or die $!;
local $/;
my $blob = <$fh>;
close $fh;
my @body = grep { !/^-----/ && /\S/ } split /\n/, $blob;
my $der = decode_base64(join "", @body);
print length($der), " bytes of DER\n";
The BEGIN and END lines are stripped, the rest is joined into one string, and every newline is ignored on the way through. For everyday certificate work the OpenSSL tooling already does this for you; the eight lines above are the pattern to remember when you need the raw DER bytes yourself, for a hash, a fingerprint, or a comparison.
Data URIs: Images That Carry Their Own Address
The data: scheme from RFC 2397 inlines a payload directly in a URL: data:, an optional media type, an optional ;base64 flag, a comma, and the data. Binary media like images use the flag, so the payload is the standard alphabet with padding, and the ordinary decoder handles it after a small cut:
use MIME::Base64 qw(decode_base64);
my $uri = "data:image/png;base64,iVBORw0KGgo...";
$uri =~ s/^data:[^,]+,// or die "not a data URI";
my $raw = decode_base64($uri);
print unpack("H8", $raw), "\n"; # 89504e47: the PNG magic bytes
Checking the magic bytes is the move. If those first four hex characters are not 89504e47, the image is not a PNG no matter what the media type claims, and a decoder that never complains makes exactly that kind of quiet lie possible.
Cousins and Fossils: uuencode and the Other Alphabets
Before Base64 won, the classic UNIX way to mail a binary was uuencode, and you will still meet it in old mailing lists and old tools. The good news: Perl has a built in decoder for it, no module required, thanks to the u template in pack and unpack:
my $uu = pack("u", "Hello, World!");
print $uu, "\n"; # -2&5L;&\L(%=O<FQD(0`` plus a newline
my $back = unpack("u", $uu);
print $back, "\n"; # Hello, World!
The two calls are exact inverses, which is the whole story you need, and the classic uuencode command from the UNIX toolchain simply wraps the bare lines in a begin header and an end footer, so the payload you are decoding is the part between them.
Base64 also has dialect cousins, and knowing which decoder eats which saves you a debugging session:
| Dialect | Wrap | Where you meet it | What decode_base64 does |
|---|---|---|---|
| MIME (RFC 2045) | 76 characters | email bodies | decodes it as is: line breaks and CRLF are ignored |
| PEM (RFC 1421) | 64 characters | certificates and keys | decodes it as is |
| PKIX (RFC 7468) | 64 characters | X.509 textual structures | decodes it as is |
| OpenPGP armor (RFC 9580) | 76 characters plus a CRC24 line | PGP keys and signatures | decodes it as is, the checksum line is just ignored |
| IMAP (RFC 3501) | none | mailbox names | not this alphabet: the slash becomes a comma, translate the letters first |
The takeaway: for every standard alphabet variant that only wraps differently, one lenient decoder covers all of them. Only when the alphabet itself changes do you need to translate the characters first.
Config, Databases and Environment Variables
Container platforms, cloud consoles, and a surprising number of configuration files store credentials and small documents as opaque Base64 strings, because a blob of letters and digits looks less dangerous than the password it is. The decode is always the same two lines: decode_base64 plus a charset decision:
use MIME::Base64 qw(decode_base64);
use Encode qw(decode);
my $secret = decode("UTF-8", decode_base64($config->{api_key}));
The reason the format is so popular in this spot is exactly the one RFC 4648 warns about: humans stop noticing that the data is readable. So treat the decoded output as confidential from the moment it is returned, and keep both the blob and its result out of log files, alerts, and debug dumps.
The same shape appears in databases, where binary data often rides in a TEXT column as Base64 because the column cannot promise to pass arbitrary bytes through untouched:
use MIME::Base64 qw(decode_base64);
my $icon = decode_base64($row->{icon_data});
open my $fh, ">:raw", "icon.png" or die $!;
print {$fh} $icon;
close $fh;
Email: MIME Parts and Attachments
Email is where Base64 got its name, and the module's leniency is designed for exactly this traffic. A MIME part with Content-Transfer-Encoding: base64 arrives as 76 character lines of CRLF terminated text, and the decoder eats the whole envelope as is, line breaks included:
use MIME::Base64 qw(decode_base64);
my $part_body = "SGVsbG8sIHF1ZXJ5IQpUaGlzIE1JTUUgcGFydCB0cmF2ZWxsZWQgYXMgYmFzZTY0LCB3cmFwcGVk";
$part_body .= "\r\nIGF0IDc2IGNoYXJhY3RlcnMsIENSTEYgYmV0d2VlbiBsaW5lcy4=";
my $text = decode_base64($part_body);
print $text; # the original two line message body
If you build or parse mail with a framework, you do none of this by hand: MIME::Lite base64 encodes an attachment for you when you pass Encoding => "base64" to attach, and Email::MIME does the same automatically. The hand rolled version above is for the mail that arrives as raw text in a log, a ticket, or a forwarded message, which in practice is a lot of it.
Pitfalls, Collected and Ranked
The module is small enough to memorize, so here is the whole trap list in one place, sorted roughly by how often it bites:
| Pitfall | What happens | Fix |
|---|---|---|
| Feeding a base64url segment to the standard decoder | the - and _ are dropped as noise, and the rest decodes into silently wrong bytes |
use decode_base64url, or translate the alphabet and restore padding first |
| Trusting the silence on corrupt input | foreign characters, truncation, and a wrong alphabet all decode without a single warning | run the strict check first, and verify with a hash when the original is available |
| Non ASCII characters inside the blob | a stray accented letter or a pasted unicode space is silently ignored, shrinking the result without comment | the same strict check rejects everything outside the 7 bit alphabet |
| Treating the result as text | the bytes carry no UTF 8 flag, so length() counts bytes and string functions get the wrong picture |
chain decode("UTF-8", $raw) or your chosen charset before any text processing |
| Double encoding on the way out | feeding already UTF 8 bytes through encode("UTF-8", ...) turns Hëllo into Hëllo |
encode characters, never raw bytes, and check the flag with utf8::is_utf8() when in doubt |
| Decoding a file line by line with ragged wraps | lines that do not end on four character boundaries produce padding in the middle of the output | slurp with -0777, or guarantee four character wrap points |
| Returning a failed regex match into a numeric context | a sub that ends in return $x =~ /.../ and feeds it to printf raises a misleading Missing argument in printf warning |
coerce the match: return $x =~ /.../ ? 1 : 0 |
| Old code that expects the old warning | scripts from before 3.11 that relied on the Premature end of base64 data carp under -w now see nothing |
add your own strict check; the warning is gone for good |
| Assuming decoding is verification | the decoder accepts almost anything and says nothing about it | a matching checksum or a verified signature is the only proof that matters |
| Logging what you decode | the format hides nothing, and the log file is exactly where the next person finds it | keep decoded secrets out of logs, alerts, and debug dumps |
Good Habits
The habits that keep Base64 from ever getting the better of your scripts:
- Expect bytes, always. Write code that knows
decode_base64returns raw octets, and chain the charsetdecodeexplicitly instead of hoping the terminal does the right thing. - Name your charset. Default to
UTF-8and switch only when the data says otherwise. The strict failure ofdecode("UTF-8", ...)is a feature: it tells you the bytes are not what you assumed. - Match the alphabet to the source.
decode_base64urlfor URLs, tokens, and IDs;decode_base64for everything else. The two alphabets are not interchangeable, and the decoder will not tell you when you pick wrong. - Validate before you decode. There is no strict mode flag in this module, so a small check is the bouncer.
- Slurp files by default.
-0777orlocal $/ = undefremoves an entire class of wrap point bugs, and the memory cost is a non issue for the files you actually decode. - Use
:rawon every file handle. Binary in, binary out. Text layers are for humans, not for bytes. - Verify with a hash. When the original is available, a matching checksum is the only proof of a byte exact decode.
- Never log what you decode. The format hides nothing.
A Short History, Told by the Changelog
The format is old, and Perl's relationship with it is older than it looks. A few checked dates, in order:
- The C code predates Perl 5. The fast decoder inside the module descends from code in metamail, the mail program from Bellcore, copyrighted in 1991, three years before the first Perl 5 release. When you call
decode_base64today, a piece of the nineties is doing the work. - Born in the web tools. The module started as
LWP::Base64inside libwww perl in the mid nineties, written by Martijn Koster and Joerg Reichelt, and it graduated to its own CPAN distribution,MIME::Base64, in April 1997, version 2.00, with the changelog entry reading based on libwww perl 5.08. - The warning era. From 2.03 in 1997, truncated input produced a Premature end of base64 data warning under
-winstead of a croak, and 2.11 in 1999 fixed the builds that warned about data that was fine. It was a more nervous decade for decoders. - Core since 2002. Perl 5.8 pulled the module into the core distribution, and the 2.13 sync with the core that same December brought EBCDIC support along, which is why the encoder and decoder still work on mainframes.
- The URL safe dialect arrived in 2010. Version 3.11 added
decode_base64urland its sibling, four years after both RFC 4648 and the standaloneMIME::Base64::URLSafemodule had landed on CPAN back in 2006. - The quieting. That same 3.11 release removed even the old truncation warning, on the grounds that the input might be perfectly intentional. Every release since, including the current 3.16 line from 2020, has kept the decoder polite and silent.
Fun Facts, Specifically Perl
To round off the tour, the trivia that makes this story a good one:
- Decode the format's own name.
decode_base64("YmFzZTY0")returnsbase64. It has been true since 1997 and will be true forever. - The decoder is a polite ghost. In its 3.x history it has never raised an exception on bad input. Corrupt, truncated, wrong alphabet: it decodes everything and complains about nothing, a behavior the changelog deliberately cemented in 2010.
- The MIME wrap is a multiple of four on purpose. The 76 character limit is 57 groups of three bytes times four characters, which is why a line by line decode is safe on any properly wrapped MIME body and unsafe on anything else.
- uuencode never prints a lowercase letter. Its alphabet tops out at the underscore, which is why old uuencoded files look like they were typed by an all caps machine, and why Perl still carries a built in decoder for a format older than the internet.
- Perl once shipped its own decode base64 command. Releases from 2.14 in 2003 through 3.05 in 2005 bundled
encode-base64,decode-base64, and their quoted printable twins as scripts; they moved to the separate MIME Base64 Scripts distribution in 2005. If you find an old install with that command on the PATH, now you know where it came from. - YouTube video IDs are base64url in disguise. The eleven character ID in your address bar is a 64 bit number in the URL safe alphabet with the padding stripped, so every video you have ever watched has a Base64 string in its URL, and
decode_base64urlcan read one. - The leniency is a standard, not a bug. The MIME rule to be liberal in what you accept is the reason this decoder survives three decades of messy data, and the reason RFC 4648 warns that the same leniency can be turned into a covert channel if you trust untrusted input.
So the next time a string of letters, digits, plus, and slash lands in your terminal, you know the whole story. One function call does the work, the decoder is a polite ghost that will never refuse you, base64url has its own decoder, the charset is a decision you make on purpose, files come in raw and go out raw, and a hash is the only proof that matters. And if one day you need to do the trip in the other direction, wrapping your own raw data in a text envelope and sending it off into the world, the related article on Base64 encoding in Perl, linked below, covers that ritual in the same depth.
Last updated: 2026-08-29
Related article: Base64 Encoding in Perl: A Complete Guide