<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Peter Anderson</title>
    <description>The latest articles on DEV Community by Peter Anderson (@pjanderson).</description>
    <link>https://dev.to/pjanderson</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4069253%2F44854b27-72e3-4288-925f-ca9ffcf18db1.png</url>
      <title>DEV Community: Peter Anderson</title>
      <link>https://dev.to/pjanderson</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pjanderson"/>
    <language>en</language>
    <item>
      <title>Debugging SAML SSO: How to Decode a SAMLResponse (and Why It's Sometimes Not XML)</title>
      <dc:creator>Peter Anderson</dc:creator>
      <pubDate>Sat, 08 Aug 2026 21:23:00 +0000</pubDate>
      <link>https://dev.to/pjanderson/debugging-saml-sso-how-to-decode-a-samlresponse-and-why-its-sometimes-not-xml-pn3</link>
      <guid>https://dev.to/pjanderson/debugging-saml-sso-how-to-decode-a-samlresponse-and-why-its-sometimes-not-xml-pn3</guid>
      <description>&lt;p&gt;You're debugging a broken SSO login. The identity provider (IdP) redirects back to your app, and somewhere in the request is a big blob called &lt;code&gt;SAMLResponse&lt;/code&gt;. You grab it, Base64-decode it, and expect to see clean XML.&lt;/p&gt;

&lt;p&gt;Sometimes you do. Sometimes you get binary garbage that starts with bytes like &lt;code&gt;0x78 0x9c&lt;/code&gt; and looks nothing like markup.&lt;/p&gt;

&lt;p&gt;Both outcomes are correct. The difference is which &lt;strong&gt;SAML binding&lt;/strong&gt; the IdP used, and once you know the two encoding chains, SAML debugging stops being guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two bindings, and their two encodings
&lt;/h2&gt;

&lt;p&gt;SAML sends its messages (&lt;code&gt;SAMLResponse&lt;/code&gt;, &lt;code&gt;SAMLRequest&lt;/code&gt;) using one of two HTTP bindings, and they encode the payload differently:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP-POST binding&lt;/strong&gt; — the message rides in a hidden form field that auto-submits via POST. The value is simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;Base64(XML)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decode the Base64 and you get the assertion XML directly. This is the common case for the response coming back from the IdP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP-Redirect binding&lt;/strong&gt; — the message rides in a URL query string, so it has to be small and URL-safe. The value is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URLEncode( Base64( DEFLATE( XML ) ) )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's three layers. If you only Base64-decode it, you're staring at the raw output of a DEFLATE compressor — which is exactly the binary garbage people report. This binding is typically used for &lt;code&gt;SAMLRequest&lt;/code&gt; (the AuthnRequest your app sends &lt;em&gt;to&lt;/em&gt; the IdP) and for Single Logout.&lt;/p&gt;

&lt;p&gt;Critically, the redirect binding uses &lt;strong&gt;raw DEFLATE&lt;/strong&gt; (RFC 1951) with &lt;strong&gt;no zlib header and no checksum&lt;/strong&gt;. That's the single most common thing people get wrong — they reach for a normal zlib/gzip inflate, it chokes on the missing header, and they conclude the blob is corrupt. It isn't; it just needs a raw inflate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoding both in Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;zlib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unquote&lt;/span&gt;

&lt;span class="c1"&gt;# --- HTTP-POST binding: Base64(XML) ---
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decode_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saml_response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saml_response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- HTTP-Redirect binding: URLEncode(Base64(DEFLATE(XML))) ---
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decode_redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saml_param&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;url_decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unquote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saml_param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# 1. undo URL-encoding
&lt;/span&gt;    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url_decoded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# 2. undo Base64
&lt;/span&gt;    &lt;span class="c1"&gt;# 3. RAW inflate: wbits = -15 =&amp;gt; no zlib header, no checksum
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;zlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The magic number is that &lt;code&gt;-15&lt;/code&gt; passed as &lt;code&gt;wbits&lt;/code&gt;. The negative sign tells zlib "this is raw DEFLATE, don't expect a header." Use &lt;code&gt;zlib.decompress(data, -15)&lt;/code&gt; for the redirect binding, plain &lt;code&gt;base64.b64decode&lt;/code&gt; for POST.&lt;/p&gt;

&lt;p&gt;Not sure which binding you're holding? A quick heuristic: if Base64-decoding gives you text starting with &lt;code&gt;&amp;lt;?xml&lt;/code&gt; or &lt;code&gt;&amp;lt;saml&lt;/code&gt;, it's POST. If it gives you bytes, try a raw inflate — it's almost certainly Redirect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually read in the decoded assertion
&lt;/h2&gt;

&lt;p&gt;Once you have XML, don't just admire it — the debugging clues are in specific fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Issuer&lt;/code&gt;&lt;/strong&gt; — is this really from the IdP you configured? A mismatch means metadata drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Destination&lt;/code&gt;&lt;/strong&gt; — must match your Assertion Consumer Service (ACS) URL. A trailing slash or &lt;code&gt;http&lt;/code&gt; vs &lt;code&gt;https&lt;/code&gt; mismatch here breaks validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;samlp:Status&amp;gt;&lt;/code&gt; &lt;code&gt;StatusCode&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;Success&lt;/code&gt; vs something like &lt;code&gt;Responder&lt;/code&gt; or &lt;code&gt;AuthnFailed&lt;/code&gt;. This is the IdP telling you &lt;em&gt;why&lt;/em&gt; it said no.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NameID&lt;/code&gt; + its &lt;code&gt;Format&lt;/code&gt;&lt;/strong&gt; — is the user identifier what your app expects (email vs persistent vs transient)? A format mismatch is a classic "logged in but no account found" bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Conditions&lt;/code&gt; &lt;code&gt;NotBefore&lt;/code&gt; / &lt;code&gt;NotOnOrAfter&lt;/code&gt;&lt;/strong&gt; — these are usually a tight window (a few minutes). If the server clocks between IdP and SP drift, validation fails with a confusing "assertion expired" even though the login just happened. &lt;strong&gt;Clock skew is one of the most common SAML failures&lt;/strong&gt;, and you can only see it by reading these timestamps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AudienceRestriction&lt;/code&gt; &lt;code&gt;Audience&lt;/code&gt;&lt;/strong&gt; — must equal your SP entity ID. If it doesn't, the IdP issued the assertion for a different application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature&lt;/strong&gt; — check whether a &lt;code&gt;&amp;lt;ds:Signature&amp;gt;&lt;/code&gt; is present. But note the trap: &lt;strong&gt;a signature being present is not the same as it being verified.&lt;/strong&gt; Presence tells you the IdP signed something; only your SP validating it against the right certificate tells you it's trustworthy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Base64 errors you'll actually hit
&lt;/h2&gt;

&lt;p&gt;If your IdP is Azure AD / Entra, you'll eventually meet &lt;strong&gt;&lt;code&gt;AADSTS750056: SAMLResponse must be a properly formed and encoded ... Base64&lt;/code&gt;&lt;/strong&gt;. Despite the wording, the assertion is usually fine — something mangled the transport:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;proxy or WAF stripping &lt;code&gt;+&lt;/code&gt; characters&lt;/strong&gt; (turning them into spaces), because &lt;code&gt;+&lt;/code&gt; is significant in Base64 but also means "space" in some URL contexts. Result: an invalid Base64 string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;76-character line wrapping&lt;/strong&gt; (MIME-style Base64) that a strict decoder rejects. Standard SAML Base64 should be one continuous string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both come down to the same fix: preserve the exact bytes end-to-end and don't let middleware "helpfully" rewrite the payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  A security warning worth repeating
&lt;/h2&gt;

&lt;p&gt;A SAML assertion &lt;strong&gt;is a credential.&lt;/strong&gt; For its short validity window, whoever holds it can often impersonate the user. So when you decode one for debugging, decode it in something that runs &lt;strong&gt;client-side&lt;/strong&gt; — never paste a production assertion into a random server-side online decoder, because you've just handed a live credential to a third-party server.&lt;/p&gt;

&lt;p&gt;For quick local decoding there's a free browser-based tool that auto-detects the binding — it'll URL-decode, Base64-decode, and auto-inflate raw DEFLATE, pretty-print the XML, and pull out the claims for you, all &lt;strong&gt;100% client-side&lt;/strong&gt; with nothing sent to a server: &lt;strong&gt;&lt;a href="https://base64.dev/saml-decoder" rel="noopener noreferrer"&gt;SAML Decoder&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  POST vs Redirect at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;HTTP-POST binding&lt;/th&gt;
&lt;th&gt;HTTP-Redirect binding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Encoding chain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Base64(XML)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;URLEncode(Base64(DEFLATE(XML)))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decode steps&lt;/td&gt;
&lt;td&gt;Base64-decode&lt;/td&gt;
&lt;td&gt;URL-decode → Base64-decode → &lt;strong&gt;raw&lt;/strong&gt; inflate (&lt;code&gt;wbits=-15&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Raw DEFLATE (no zlib header)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typically used for&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SAMLResponse&lt;/code&gt; from IdP&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SAMLRequest&lt;/code&gt;, Single Logout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base64-decode alone gives&lt;/td&gt;
&lt;td&gt;Clean XML&lt;/td&gt;
&lt;td&gt;Binary garbage (DEFLATE bytes)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;SAMLResponse&lt;/code&gt; that decodes to clean XML is the &lt;strong&gt;POST binding&lt;/strong&gt; (&lt;code&gt;Base64(XML)&lt;/code&gt;). One that decodes to binary is the &lt;strong&gt;Redirect binding&lt;/strong&gt; (&lt;code&gt;Base64(DEFLATE(XML))&lt;/code&gt;, URL-encoded).&lt;/li&gt;
&lt;li&gt;For the redirect binding you must &lt;strong&gt;URL-decode, Base64-decode, then RAW-inflate&lt;/strong&gt; — &lt;code&gt;zlib.decompress(data, -15)&lt;/code&gt;. Regular zlib/gzip inflate will fail on the missing header.&lt;/li&gt;
&lt;li&gt;When reading the assertion, check &lt;code&gt;Status&lt;/code&gt;, &lt;code&gt;Destination&lt;/code&gt;, &lt;code&gt;Audience&lt;/code&gt;, and especially the &lt;code&gt;Conditions&lt;/code&gt; timestamps — &lt;strong&gt;clock skew&lt;/strong&gt; is a top cause of "it authenticated but still failed."&lt;/li&gt;
&lt;li&gt;Treat assertions as live credentials: decode them &lt;strong&gt;client-side&lt;/strong&gt;, never on someone else's server.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AADSTS750056&lt;/code&gt; usually means a proxy mangled &lt;code&gt;+&lt;/code&gt; or line-wrapped the Base64, not a malformed assertion.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;What's the weirdest SAML failure you've had to decode your way out of? Share it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>debugging</category>
      <category>security</category>
    </item>
    <item>
      <title>Decoding a PowerShell -EncodedCommand During Incident Response (the UTF-16 gotcha)</title>
      <dc:creator>Peter Anderson</dc:creator>
      <pubDate>Sat, 08 Aug 2026 21:22:00 +0000</pubDate>
      <link>https://dev.to/pjanderson/decoding-a-powershell-encodedcommand-during-incident-response-the-utf-16-gotcha-5a6i</link>
      <guid>https://dev.to/pjanderson/decoding-a-powershell-encodedcommand-during-incident-response-the-utf-16-gotcha-5a6i</guid>
      <description>&lt;p&gt;You're triaging an alert. Scheduled task, weird parent process, and a command line that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;powershell.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-nop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-enc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;JABjACAAPQAg...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You know the drill: grab the Base64 blob, decode it, read the script. So you paste it into a decoder and get back this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;c   &lt;span class="o"&gt;=&lt;/span&gt;   &lt;span class="s2"&gt;" h t t p : / / ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Garbage. A space (or a null) between every single character. First instinct is that the payload is doubly-encoded or encrypted. It isn't. &lt;strong&gt;This is the single most common gotcha with &lt;code&gt;-EncodedCommand&lt;/code&gt;, and once you know it, it takes ten seconds to fix.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it looks garbled
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;powershell.exe -enc&lt;/code&gt; (short for &lt;code&gt;-EncodedCommand&lt;/code&gt;) expects Base64 of &lt;strong&gt;UTF-16LE&lt;/strong&gt; (little-endian Unicode) bytes — not UTF-8. That's mandated by PowerShell itself, not a choice the attacker made.&lt;/p&gt;

&lt;p&gt;In UTF-16LE, every ASCII character is stored as &lt;strong&gt;two bytes&lt;/strong&gt;: the character followed by a &lt;code&gt;0x00&lt;/code&gt; null byte. So the letter &lt;code&gt;c&lt;/code&gt; isn't &lt;code&gt;0x63&lt;/code&gt;, it's &lt;code&gt;0x63 0x00&lt;/code&gt;. When you Base64-decode the blob and then read it as UTF-8, every one of those null bytes renders as a space or an invisible control character. Hence the &lt;code&gt;h t t p&lt;/code&gt; spacing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Text:      c        =        "
UTF-16LE:  63 00    3D 00    22 00
UTF-8 view: c  ␀     =  ␀     "  ␀     &amp;lt;- the null shows up as a "space"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decode it as UTF-16LE instead and the nulls disappear, because that's what they were: the high byte of each 16-bit code unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decode it correctly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In PowerShell itself&lt;/strong&gt; — the encoding is literally called &lt;code&gt;Unicode&lt;/code&gt; in .NET, which means UTF-16LE:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$enc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'JABjACAAPQAg...'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.Text.Encoding&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Unicode.GetString&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;System.Convert&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;FromBase64String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$enc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In Python&lt;/strong&gt; — decode the bytes, then read them as &lt;code&gt;utf-16-le&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JABjACAAPQAg...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-16-le&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In CyberChef&lt;/strong&gt; — build the recipe &lt;code&gt;From Base64&lt;/code&gt; → &lt;code&gt;Decode text (UTF-16LE)&lt;/code&gt;. Or &lt;code&gt;From Base64&lt;/code&gt; then &lt;code&gt;Remove null bytes&lt;/code&gt; for a quick-and-dirty look.&lt;/p&gt;

&lt;p&gt;Any of these turns the spaced-out mess back into readable PowerShell.&lt;/p&gt;

&lt;h2&gt;
  
  
  The encode direction (for building test cases)
&lt;/h2&gt;

&lt;p&gt;If you're writing detections or a lab sample, this is how the blob is produced — same &lt;code&gt;Unicode&lt;/code&gt;/UTF-16LE contract in reverse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Write-Host "hello from encoded command"'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.Convert&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ToBase64String&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;System.Text.Encoding&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Unicode.GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feed that output back to &lt;code&gt;powershell -enc&lt;/code&gt; and it runs. Legit automation uses this all the time — it's a clean way to pass a multi-line script through a single argument without quoting nightmares. That's exactly why malware hides in the same crowd.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second layer: it's often gzip too
&lt;/h2&gt;

&lt;p&gt;Sometimes you decode correctly and &lt;em&gt;still&lt;/em&gt; get binary garbage. That's the next common trick: the real script is &lt;strong&gt;gzip-compressed&lt;/strong&gt;, then Base64'd. The outer PowerShell you just decoded is a tiny loader whose whole job is to inflate the inner payload in memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# telltale pattern inside the decoded stub&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IO.MemoryStream&lt;/span&gt;&lt;span class="p"&gt;(,[&lt;/span&gt;&lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;FromBase64String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"H4sIA..."&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;IO.StreamReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IO.Compression.GzipStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;IO.Compression.CompressionMode&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Decompress&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadToEnd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tell is the inner Base64 starting with &lt;code&gt;H4sI&lt;/code&gt; — that's the gzip magic number (&lt;code&gt;1f 8b&lt;/code&gt;) in Base64. When you see it, decode Base64 again and gunzip to reach the actual script. Payloads can nest two or three layers deep this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it safe to decode a suspicious sample?
&lt;/h2&gt;

&lt;p&gt;Yes. &lt;strong&gt;Decoding is not executing.&lt;/strong&gt; Converting Base64 back to text and inflating gzip are pure data transformations — no &lt;code&gt;powershell.exe&lt;/code&gt; runs, nothing touches the script block, nothing hits the network. The danger is only if you &lt;em&gt;run&lt;/em&gt; the decoded command. So decode freely and read; just don't pipe the result into a shell.&lt;/p&gt;

&lt;p&gt;For fast triage there's a free browser-based tool that decodes &lt;code&gt;-enc&lt;/code&gt; / &lt;code&gt;-EncodedCommand&lt;/code&gt; for you — it handles the UTF-16LE conversion and auto-inflates nested gzip layers, and it runs entirely client-side so it's safe to paste suspicious samples into (nothing is uploaded): &lt;strong&gt;&lt;a href="https://base64.dev/powershell-encoded-command-decoder" rel="noopener noreferrer"&gt;PowerShell Encoded Command Decoder&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analyst checklist vs. the layers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see after decoding&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Space between every char&lt;/td&gt;
&lt;td&gt;You decoded as UTF-8&lt;/td&gt;
&lt;td&gt;Re-decode as &lt;strong&gt;UTF-16LE&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean, readable PowerShell&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;td&gt;Read it, extract IOCs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary garbage, starts &lt;code&gt;H4sI&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Inner &lt;strong&gt;gzip&lt;/strong&gt; payload&lt;/td&gt;
&lt;td&gt;Base64-decode again, then gunzip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Another &lt;code&gt;-enc&lt;/code&gt; / &lt;code&gt;FromBase64String&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Nested loader&lt;/td&gt;
&lt;td&gt;Repeat the whole process on the inner blob&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;powershell -enc &amp;lt;base64&amp;gt;&lt;/code&gt; is Base64 of &lt;strong&gt;UTF-16LE&lt;/strong&gt;, not UTF-8 — that's why naive decoding shows a space/null between every character.&lt;/li&gt;
&lt;li&gt;Decode it right with &lt;code&gt;[System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($enc))&lt;/code&gt;, or in Python with &lt;code&gt;base64.b64decode(enc).decode("utf-16-le")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the result is still binary and starts with &lt;code&gt;H4sI&lt;/code&gt;, it's &lt;strong&gt;gzip inside Base64&lt;/strong&gt; — inflate it to reach the real script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decoding never executes&lt;/strong&gt; the command, so it's safe to analyze suspicious samples locally.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Seen a &lt;code&gt;-enc&lt;/code&gt; payload with an interesting nesting trick? Share the pattern in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>infosec</category>
      <category>security</category>
    </item>
    <item>
      <title>Sending Images to GPT-4o, Claude, and Gemini: The Base64 Payload Each One Wants</title>
      <dc:creator>Peter Anderson</dc:creator>
      <pubDate>Sat, 08 Aug 2026 21:09:29 +0000</pubDate>
      <link>https://dev.to/pjanderson/sending-images-to-gpt-4o-claude-and-gemini-the-base64-payload-each-one-wants-1iei</link>
      <guid>https://dev.to/pjanderson/sending-images-to-gpt-4o-claude-and-gemini-the-base64-payload-each-one-wants-1iei</guid>
      <description>&lt;p&gt;You want to send a screenshot to a vision model. All three of the big ones — OpenAI's GPT-4o, Anthropic's Claude, Google's Gemini — accept images the same fundamental way: &lt;strong&gt;Base64-encode the bytes and put them in the JSON request.&lt;/strong&gt; No file uploads, no multipart, just text in a payload.&lt;/p&gt;

&lt;p&gt;And yet the single most common error people hit is some flavor of &lt;code&gt;invalid image&lt;/code&gt; / &lt;code&gt;could not process image&lt;/code&gt;. The reason is almost never the image. It's that &lt;strong&gt;each provider wants the Base64 wrapped in a differently shaped object&lt;/strong&gt;, and the traps are subtle — especially the &lt;code&gt;data:&lt;/code&gt; URL prefix, which one provider requires and the other two reject.&lt;/p&gt;

&lt;p&gt;Here's the exact payload each one wants, side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenAI (GPT-4o)
&lt;/h2&gt;

&lt;p&gt;GPT-4o uses a &lt;code&gt;content&lt;/code&gt; array of parts. The image is an &lt;code&gt;image_url&lt;/code&gt; part, and — this is the trap — the &lt;code&gt;url&lt;/code&gt; field takes a &lt;strong&gt;full data URL&lt;/strong&gt;, prefix and all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;photo.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;standard_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s in this image?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data:image/png;base64,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b64&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The literal payload shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image_url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"image_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data:image/png;base64,&amp;lt;BASE64&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;data:image/png;base64,&lt;/code&gt; &lt;strong&gt;is&lt;/strong&gt; part of the value. Send raw Base64 here and it fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anthropic (Claude)
&lt;/h2&gt;

&lt;p&gt;Claude uses an &lt;code&gt;image&lt;/code&gt; content block with a &lt;code&gt;source&lt;/code&gt; object. Here the MIME type is a &lt;strong&gt;separate field&lt;/strong&gt; (&lt;code&gt;media_type&lt;/code&gt;), and the &lt;code&gt;data&lt;/code&gt; field wants &lt;strong&gt;raw Base64 — no &lt;code&gt;data:&lt;/code&gt; prefix&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;photo.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;standard_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base64&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# raw, NO data: prefix
&lt;/span&gt;                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s in this image?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The literal payload shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"base64"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"media_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;BASE64&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you paste a &lt;code&gt;data:image/png;base64,...&lt;/code&gt; string into &lt;code&gt;data&lt;/code&gt; here, Claude will reject it — the prefix is not valid Base64.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google (Gemini)
&lt;/h2&gt;

&lt;p&gt;Gemini uses &lt;code&gt;parts&lt;/code&gt; with an &lt;code&gt;inline_data&lt;/code&gt; object: a &lt;code&gt;mime_type&lt;/code&gt; field plus &lt;strong&gt;raw Base64&lt;/strong&gt; (again, no prefix):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.genai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;photo.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;image_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;image_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mime_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s in this image?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK handles the encoding for you above, but the wire payload it builds is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inline_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mime_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;BASE64&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more Gemini gotcha: the &lt;strong&gt;JavaScript SDK camelCases these keys&lt;/strong&gt; — it's &lt;code&gt;inlineData&lt;/code&gt; and &lt;code&gt;mimeType&lt;/code&gt;, not &lt;code&gt;inline_data&lt;/code&gt; / &lt;code&gt;mime_type&lt;/code&gt;. Same structure, different casing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three shapes at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Field the image goes in&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;data:&lt;/code&gt; prefix?&lt;/th&gt;
&lt;th&gt;Raw Base64?&lt;/th&gt;
&lt;th&gt;MIME type lives in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenAI (GPT-4o)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;image_url.url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; — full data URL&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Inside the data URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Anthropic (Claude)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source.data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source.media_type&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google (Gemini)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inline_data.data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inline_data.mime_type&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read it top to bottom: OpenAI is the odd one out that wants the whole &lt;code&gt;data:&lt;/code&gt; URL; Claude and Gemini both want raw Base64 with the MIME type broken out into its own field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you're getting "invalid image"
&lt;/h2&gt;

&lt;p&gt;When a request fails, it's almost always one of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stray &lt;code&gt;data:&lt;/code&gt; prefix where raw Base64 is expected.&lt;/strong&gt; Sending &lt;code&gt;data:image/png;base64,iVBOR...&lt;/code&gt; to Claude or Gemini's &lt;code&gt;data&lt;/code&gt; field. They want just &lt;code&gt;iVBOR...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing &lt;code&gt;data:&lt;/code&gt; prefix where OpenAI wants it.&lt;/strong&gt; Sending raw Base64 to GPT-4o's &lt;code&gt;image_url.url&lt;/code&gt;. It wants the full data URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mismatched MIME type.&lt;/strong&gt; Declaring &lt;code&gt;image/jpeg&lt;/code&gt; for a PNG (or vice versa). The declared type must match the actual bytes. A telltale sign: your Base64 starts with &lt;code&gt;iVBORw0KGgo&lt;/code&gt; (PNG) but you labeled it &lt;code&gt;image/jpeg&lt;/code&gt; (JPEG bytes start &lt;code&gt;/9j/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whitespace or newlines in the Base64.&lt;/strong&gt; Some encoders (looking at you, &lt;code&gt;base64&lt;/code&gt; CLI without &lt;code&gt;-w0&lt;/code&gt;, and older MIME encoders) wrap output at 76 columns. Strip newlines before sending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsupported format.&lt;/strong&gt; Stick to PNG, JPEG, WebP, and non-animated GIF. HEIC, SVG, TIFF, and animated GIFs are frequent rejects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too large.&lt;/strong&gt; OpenAI and Claude cap around &lt;strong&gt;~5 MB per image&lt;/strong&gt;; Gemini allows up to &lt;strong&gt;~20 MB of inline data per request&lt;/strong&gt;, and above that you're expected to use its Files API instead of inline Base64. Remember Base64 inflates size by ~33%, so a 4 MB file is ~5.3 MB on the wire.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you just need to grab a correct Base64 string and see the exact payload for each provider without wiring up a script, there's a free browser tool that encodes an image locally (nothing gets uploaded) and shows the ready-to-paste block for all three: &lt;strong&gt;&lt;a href="https://base64.dev/image-to-base64-for-ai-vision" rel="noopener noreferrer"&gt;Image to Base64 for AI Vision&lt;/a&gt;&lt;/strong&gt;. There are also provider-specific walkthroughs for &lt;a href="https://base64.dev/claude-api-base64-image" rel="noopener noreferrer"&gt;Claude&lt;/a&gt; and &lt;a href="https://base64.dev/gemini-api-base64-image" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt; if you only care about one.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All three&lt;/strong&gt; vision APIs take images as Base64 in the JSON body — no uploads needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT-4o&lt;/strong&gt; wants a &lt;strong&gt;full &lt;code&gt;data:&lt;/code&gt; URL&lt;/strong&gt; in &lt;code&gt;image_url.url&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude&lt;/strong&gt; wants &lt;strong&gt;raw Base64&lt;/strong&gt; in &lt;code&gt;source.data&lt;/code&gt;, with the type in &lt;code&gt;source.media_type&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini&lt;/strong&gt; wants &lt;strong&gt;raw Base64&lt;/strong&gt; in &lt;code&gt;inline_data.data&lt;/code&gt;, with the type in &lt;code&gt;inline_data.mime_type&lt;/code&gt; (&lt;code&gt;inlineData&lt;/code&gt;/&lt;code&gt;mimeType&lt;/code&gt; in the JS SDK).&lt;/li&gt;
&lt;li&gt;Most &lt;code&gt;invalid image&lt;/code&gt; errors are a &lt;strong&gt;misplaced &lt;code&gt;data:&lt;/code&gt; prefix&lt;/strong&gt;, a &lt;strong&gt;mismatched MIME type&lt;/strong&gt;, &lt;strong&gt;whitespace&lt;/strong&gt;, or a file &lt;strong&gt;over the size cap&lt;/strong&gt; (~5 MB OpenAI/Claude, ~20 MB Gemini inline).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get the wrapper right and vision "just works" across all three.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Which provider's payload has bitten you the hardest? Share your worst &lt;code&gt;invalid image&lt;/code&gt; story in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>llm</category>
      <category>openai</category>
    </item>
    <item>
      <title>Kubernetes Secrets Are Just Base64 Not Encryption. Here's What That Actually Means</title>
      <dc:creator>Peter Anderson</dc:creator>
      <pubDate>Sat, 08 Aug 2026 20:55:31 +0000</pubDate>
      <link>https://dev.to/pjanderson/kubernetes-secrets-are-just-base64-not-encryption-heres-what-that-actually-means-35hi</link>
      <guid>https://dev.to/pjanderson/kubernetes-secrets-are-just-base64-not-encryption-heres-what-that-actually-means-35hi</guid>
      <description>&lt;p&gt;If you've run Kubernetes for more than a day, you've seen this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-credentials&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YWRtaW4=&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;c3VwZXJzZWNyZXQ=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And somewhere in the back of your mind you filed it under "encrypted credentials." It isn't. &lt;strong&gt;Those values are Base64, and Base64 is encoding, not encryption.&lt;/strong&gt; &lt;code&gt;YWRtaW4=&lt;/code&gt; is just &lt;code&gt;admin&lt;/code&gt; written in a different alphabet — reversible instantly, by anyone, with no key.&lt;/p&gt;

&lt;p&gt;This trips up an astonishing number of teams, so let's clear it up for good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove it in one command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get secret db-credentials &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data.password}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;--decode&lt;/span&gt;
&lt;span class="c"&gt;# supersecret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No key. No password. No "decryption." Base64 is a &lt;strong&gt;binary-to-text encoding&lt;/strong&gt; — its entire job is to represent arbitrary bytes using a safe 64-character alphabet so they survive transport and storage in text-based systems (etcd, YAML, JSON, HTTP headers). Kubernetes encodes Secret &lt;code&gt;data&lt;/code&gt; values purely so binary values (certs, keys, gzip blobs) can live inside a YAML/JSON object. That's it. Security was never the point.&lt;/p&gt;

&lt;p&gt;If you want to eyeball a whole Secret at once instead of decoding fields one by one, I built a small in-browser tool for exactly this — paste the YAML and it decodes every &lt;code&gt;data:&lt;/code&gt; value locally (nothing is uploaded): &lt;strong&gt;&lt;a href="https://base64.dev/kubernetes-secret-decoder" rel="noopener noreferrer"&gt;Kubernetes Secret Decoder&lt;/a&gt;&lt;/strong&gt;. &lt;em&gt;(Disclosure: it's my free, no-ads tool.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;data&lt;/code&gt; vs &lt;code&gt;stringData&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A quick related gotcha: &lt;code&gt;data&lt;/code&gt; expects &lt;strong&gt;Base64&lt;/strong&gt;, but &lt;code&gt;stringData&lt;/code&gt; expects &lt;strong&gt;plain text&lt;/strong&gt; and Kubernetes Base64-encodes it for you on write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stringData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supersecret&lt;/span&gt;   &lt;span class="c1"&gt;# plain text; k8s encodes it into data.password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both end up identically un-secret at rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what actually protects a Secret?
&lt;/h2&gt;

&lt;p&gt;Base64 gets you nothing here. Real protection is layered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Encryption at rest for etcd&lt;/strong&gt; — configure a &lt;code&gt;KMS&lt;/code&gt; provider (AWS/GCP/Azure KMS) or at minimum &lt;code&gt;aescbc&lt;/code&gt;/&lt;code&gt;secretbox&lt;/code&gt; via an &lt;code&gt;EncryptionConfiguration&lt;/code&gt;. Without this, Secrets sit in etcd Base64-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sealed Secrets&lt;/strong&gt; (Bitnami) — encrypt secrets &lt;em&gt;before&lt;/em&gt; they hit Git; only the in-cluster controller can decrypt. Safe to commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOPS&lt;/strong&gt; (+ age/KMS) — encrypt values in your manifests/GitOps repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External secret stores&lt;/strong&gt; — HashiCorp Vault, AWS Secrets Manager, etc., pulled in via the External Secrets Operator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RBAC&lt;/strong&gt; — lock down who can &lt;code&gt;get&lt;/code&gt;/&lt;code&gt;list&lt;/code&gt; Secrets. If a ServiceAccount can read the Secret, it can read the plaintext.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Encoding vs encryption vs hashing (the mental model)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Encoding (Base64)&lt;/th&gt;
&lt;th&gt;Encryption (AES-GCM)&lt;/th&gt;
&lt;th&gt;Hashing (SHA-256)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Safe transport of bytes&lt;/td&gt;
&lt;td&gt;Confidentiality&lt;/td&gt;
&lt;td&gt;Integrity / fingerprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs a key?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reversible?&lt;/td&gt;
&lt;td&gt;Yes, trivially&lt;/td&gt;
&lt;td&gt;Yes, with the key&lt;/td&gt;
&lt;td&gt;No (one-way)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hides data?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Base64 is in the left column. Kubernetes Secrets, out of the box, are in the left column. If you want to go deeper on that distinction, here is a helpful article: &lt;strong&gt;&lt;a href="https://base64.dev/articles/is-base64-encryption" rel="noopener noreferrer"&gt;Is Base64 Encryption?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Kubernetes Secret's &lt;code&gt;data&lt;/code&gt; is &lt;strong&gt;Base64, not encrypted&lt;/strong&gt; — decode it with one &lt;code&gt;base64 -d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Treat any Secret YAML as &lt;strong&gt;plaintext credentials&lt;/strong&gt;: don't commit it, lock down RBAC.&lt;/li&gt;
&lt;li&gt;For actual protection: &lt;strong&gt;encryption at rest (KMS)&lt;/strong&gt;, &lt;strong&gt;Sealed Secrets&lt;/strong&gt;, &lt;strong&gt;SOPS&lt;/strong&gt;, or an &lt;strong&gt;external secret store&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next time someone says "it's fine, it's in a Secret," you'll know to ask the real question: &lt;em&gt;is etcd encrypted, and who has RBAC to read it?&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's bitten you with Kubernetes Secrets? Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>security</category>
    </item>
  </channel>
</rss>
