<?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: Presendapp</title>
    <description>The latest articles on DEV Community by Presendapp (@presend).</description>
    <link>https://dev.to/presend</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%2F4096008%2F532e1fbc-40d1-4c2b-a227-7d0493d98886.png</url>
      <title>DEV Community: Presendapp</title>
      <link>https://dev.to/presend</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/presend"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>Presendapp</dc:creator>
      <pubDate>Sat, 19 Sep 2026 10:02:01 +0000</pubDate>
      <link>https://dev.to/presend/-2kn3</link>
      <guid>https://dev.to/presend/-2kn3</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/presend/i-tried-to-check-if-our-new-blockchain-tools-were-being-used-the-tool-that-would-have-told-me-was-2b66" class="crayons-story__hidden-navigation-link"&gt;I tried to check if our new blockchain tools were being used. The tool that would have told me was also silently broken — and so were two other things.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/presend" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096008%2F532e1fbc-40d1-4c2b-a227-7d0493d98886.png" alt="presend profile" class="crayons-avatar__image" width="512" height="512"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/presend" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Presendapp
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Presendapp
                
                
              
              &lt;div id="story-author-preview-content-4693042" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/presend" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096008%2F532e1fbc-40d1-4c2b-a227-7d0493d98886.png" class="crayons-avatar__image" alt="" width="512" height="512"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Presendapp&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/presend/i-tried-to-check-if-our-new-blockchain-tools-were-being-used-the-tool-that-would-have-told-me-was-2b66" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 19&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/presend/i-tried-to-check-if-our-new-blockchain-tools-were-being-used-the-tool-that-would-have-told-me-was-2b66" id="article-link-4693042"&gt;
          I tried to check if our new blockchain tools were being used. The tool that would have told me was also silently broken — and so were two other things.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/serverless"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;serverless&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/presend/i-tried-to-check-if-our-new-blockchain-tools-were-being-used-the-tool-that-would-have-told-me-was-2b66" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/presend/i-tried-to-check-if-our-new-blockchain-tools-were-being-used-the-tool-that-would-have-told-me-was-2b66#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>I tried to check if our new blockchain tools were being used. The tool that would have told me was also silently broken — and so were two other things.</title>
      <dc:creator>Presendapp</dc:creator>
      <pubDate>Sat, 19 Sep 2026 10:01:42 +0000</pubDate>
      <link>https://dev.to/presend/i-tried-to-check-if-our-new-blockchain-tools-were-being-used-the-tool-that-would-have-told-me-was-2b66</link>
      <guid>https://dev.to/presend/i-tried-to-check-if-our-new-blockchain-tools-were-being-used-the-tool-that-would-have-told-me-was-2b66</guid>
      <description>&lt;h1&gt;
  
  
  The tool I built to check if anyone used our API was itself silently broken — and so were two other things
&lt;/h1&gt;

&lt;h1&gt;
  
  
  security #cloudflare #webdev #serverless
&lt;/h1&gt;

&lt;p&gt;Yesterday I shipped three new endpoints (a Cosmos SDK transaction decoder, an EVM address OFAC sanctions check, and a CometBFT RPC safety auditor). Today I wanted a simple answer: is anyone actually calling them?&lt;/p&gt;

&lt;p&gt;We have a &lt;code&gt;/api/api-stats&lt;/code&gt; endpoint for exactly this — reads usage counters out of a Cloudflare KV namespace. I called it.&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="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cannot read properties of undefined (reading 'list')"&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 endpoint was crashing. &lt;code&gt;env.PRESEND_ANALYTICS&lt;/code&gt; was undefined in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that actually meant
&lt;/h2&gt;

&lt;p&gt;Every other endpoint in the codebase checks for this binding defensively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRESEND_ANALYTICS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and fails open, so nothing crashes if it's missing. That's good design for resilience. It's bad news for &lt;em&gt;finding out&lt;/em&gt; it's missing, because fail-open means silence, not an error you'd notice.&lt;/p&gt;

&lt;p&gt;Checking the Cloudflare dashboard: the KV namespace existed. It had just never been bound to the Pages project — a &lt;code&gt;wrangler.toml&lt;/code&gt; requirement I didn't know applied here until the dashboard told me "bindings for this project are managed via wrangler.toml," a setting that quietly overrides the usual dashboard UI.&lt;/p&gt;

&lt;p&gt;Once bound, &lt;code&gt;api-stats&lt;/code&gt; came back to life: 4,852 real API calls over three weeks — real usage of a project I'd been half-treating as "mostly a listing exercise." Also confirmed, less happily: rate limiting on every endpoint had been a no-op the entire time, since it degrades the exact same silent way.&lt;/p&gt;

&lt;p&gt;That would have been the whole post. Then I checked the feedback form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more, found by asking "what else fails this same way?"
&lt;/h2&gt;

&lt;p&gt;Same question, different feature: does anyone's feedback actually get saved?&lt;/p&gt;

&lt;p&gt;The database backing &lt;code&gt;/api/feedback&lt;/code&gt; was a D1 instance, created weeks ago, sitting at &lt;strong&gt;0 tables&lt;/strong&gt;. The code inserts into a &lt;code&gt;feedback&lt;/code&gt; table that was never created. Every submission had been silently discarded since launch — except, it turned out, one: a single row from mid-August that predated whatever broke the binding:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Great tool, works perfectly!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First real, unprompted feedback the project had ever received, and I only found it while debugging something else entirely.&lt;/p&gt;

&lt;p&gt;Fixed the binding, ran the missing &lt;code&gt;CREATE TABLE&lt;/code&gt;, tested the form. It failed with:&lt;/p&gt;

&lt;p&gt;The CSP header allowed scripts from our own CDN sources but not from &lt;code&gt;challenges.cloudflare.com&lt;/code&gt; — so the anti-bot widget itself had been blocked from loading by our own security header, since the day the feedback feature launched.&lt;/p&gt;

&lt;p&gt;Fixed the CSP. Tried again:&lt;/p&gt;

&lt;p&gt;No error detail, just a boolean. I temporarily added Cloudflare's own &lt;code&gt;error-codes&lt;/code&gt; field to the response and got the real answer:&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="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Turnstile verification failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"debug_error_codes"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"invalid-input-secret"&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;code&gt;invalid-input-secret&lt;/code&gt;. The secret key stored in the dashboard didn't match the site key in use — copy-paste had grabbed the wrong field, twice, before we got a clean copy through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things, same failure shape
&lt;/h2&gt;

&lt;p&gt;Four separate bugs, over maybe two hours, that all failed the exact same way: silently, by design, because the failure mode we'd built (fail open, don't crash) is indistinguishable from "everything is fine" unless you go looking.&lt;/p&gt;

&lt;p&gt;Fail-open is the right call for user-facing behavior — a missing analytics binding shouldn't break rate limiting for real traffic. But it means you need a &lt;em&gt;separate&lt;/em&gt;, loud check that these bindings exist, run on a schedule, that pages you specifically when the answer is "missing" — not a check that's only as good as someone manually calling &lt;code&gt;/api/api-stats&lt;/code&gt; and noticing the crash.&lt;/p&gt;

&lt;p&gt;I don't have that yet. It's next.&lt;/p&gt;

&lt;p&gt;If you run anything on Cloudflare Workers/Pages with KV or D1 bindings: how do you actually verify they're wired up in production, rather than just handling their absence gracefully in the code? Genuinely curious if there's a standard pattern here I'm missing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>webdev</category>
      <category>serverless</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Presendapp</dc:creator>
      <pubDate>Mon, 14 Sep 2026 19:27:18 +0000</pubDate>
      <link>https://dev.to/presend/-g6b</link>
      <guid>https://dev.to/presend/-g6b</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/presend/our-ssrf-guard-passed-every-test-we-ran-until-a-strangers-comment-pointed-out-the-test-we-never-38m" class="crayons-story__hidden-navigation-link"&gt;Our SSRF guard passed every test we ran — until a stranger's comment pointed out the test we never ran&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/presend" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096008%2F532e1fbc-40d1-4c2b-a227-7d0493d98886.png" alt="presend profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/presend" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Presendapp
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Presendapp
                
                
              
              &lt;div id="story-author-preview-content-4652658" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/presend" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096008%2F532e1fbc-40d1-4c2b-a227-7d0493d98886.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Presendapp&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/presend/our-ssrf-guard-passed-every-test-we-ran-until-a-strangers-comment-pointed-out-the-test-we-never-38m" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 14&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/presend/our-ssrf-guard-passed-every-test-we-ran-until-a-strangers-comment-pointed-out-the-test-we-never-38m" id="article-link-4652658"&gt;
          Our SSRF guard passed every test we ran — until a stranger's comment pointed out the test we never ran
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/api"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;api&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/presend/our-ssrf-guard-passed-every-test-we-ran-until-a-strangers-comment-pointed-out-the-test-we-never-38m" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;8&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/presend/our-ssrf-guard-passed-every-test-we-ran-until-a-strangers-comment-pointed-out-the-test-we-never-38m#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              3&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Our SSRF guard passed every test we ran — until a stranger's comment pointed out the test we never ran</title>
      <dc:creator>Presendapp</dc:creator>
      <pubDate>Mon, 14 Sep 2026 19:26:57 +0000</pubDate>
      <link>https://dev.to/presend/our-ssrf-guard-passed-every-test-we-ran-until-a-strangers-comment-pointed-out-the-test-we-never-38m</link>
      <guid>https://dev.to/presend/our-ssrf-guard-passed-every-test-we-ran-until-a-strangers-comment-pointed-out-the-test-we-never-38m</guid>
      <description>&lt;p&gt;Three weeks ago, in the comments under a post about a different SSRF bug (CVE-2026-19304, a parser-confusion issue), someone asked a sharp question about our own URL-fetching endpoints. I answered honestly that we'd only tested that our hostname-validation and the actual fetch agreed on parsing the same string — and that whether a validated hostname could resolve to something different by the time we actually connected was a real gap we hadn't checked. I said I'd go find out whether Cloudflare Workers even supports pinning a connection to a specific IP.&lt;/p&gt;

&lt;p&gt;Then I didn't follow up. The comment sat there for weeks.&lt;/p&gt;

&lt;p&gt;Going back to it tonight, the gap was worse than I'd worried.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;isBlockedHostname()&lt;/code&gt; actually checked
&lt;/h2&gt;

&lt;p&gt;Five of our endpoints (redirect-trace, security-scan, security-headers, favicon, scrape) fetch a user-supplied URL. All five guarded against SSRF the same way: a regex blocklist checked against the hostname string before fetching.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;js&lt;br&gt;
function isBlockedHostname(hostname) {&lt;br&gt;
  return BLOCKED_HOSTNAME_PATTERNS.some((re) =&amp;gt; re.test(hostname));&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This blocks &lt;code&gt;"127.0.0.1"&lt;/code&gt; and &lt;code&gt;"localhost"&lt;/code&gt; directly. It does nothing for a hostname that &lt;em&gt;resolves to&lt;/em&gt; 127.0.0.1. The check and the actual &lt;code&gt;fetch()&lt;/code&gt; are two separate, uncontrolled DNS lookups, with a window between them where nothing stops the second lookup from answering differently than the first — classic DNS rebinding.&lt;/p&gt;

&lt;p&gt;I tested this against a live rebinding domain (rbndr.us, which alternates its DNS answer between 127.0.0.1 and a public IP depending on which query hits it) against our own production endpoint. It went straight through.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: don't trust a second DNS lookup you don't control
&lt;/h2&gt;

&lt;p&gt;Cloudflare Workers exposes exactly what's needed for this: &lt;code&gt;cf.resolveOverride&lt;/code&gt; on the &lt;code&gt;fetch()&lt;/code&gt; request options. You resolve the hostname yourself, validate the IP you actually got back, then pin the real connection to that exact address — the &lt;code&gt;Host&lt;/code&gt; header still matches the original URL, but no second, attacker-influenced DNS lookup happens between validation and connection.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;js&lt;br&gt;
async function validateAndResolve(hostname) {&lt;br&gt;
  if (isBlocked(hostname)) throw new Error(\&lt;/code&gt;Blocked hostname: \${hostname}&lt;code&gt;);&lt;br&gt;
  const ips = await resolveViaDoH(hostname); // Cloudflare's own DoH resolver&lt;br&gt;
  const blockedIp = ips.find(isBlocked);&lt;br&gt;
  if (blockedIp) throw new Error(\&lt;/code&gt;Resolves to a blocked address: \${blockedIp}`);&lt;br&gt;
  return ips[0];&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;async function safeFetch(url, options = {}) {&lt;br&gt;
  const { hostname } = new URL(url);&lt;br&gt;
  const ip = await validateAndResolve(hostname);&lt;br&gt;
  return fetch(url, { ...options, cf: { resolveOverride: ip } });&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ran this against the same rebinding domain, five times in a row: rejected every time, with the specific blocked IP named in the error. A normal external host still worked exactly as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second bug, found by the fix looking suspicious
&lt;/h2&gt;

&lt;p&gt;Wiring this into the endpoints that follow redirects (not just the initial URL — a redirect chain that starts safe and pivots partway through is the same bug one hop later), one of them started returning results that looked wrong: a real HTTP grade and score, for a URL that should have been blocked.&lt;/p&gt;

&lt;p&gt;Before assuming the fix was broken, I checked what it was actually connecting to. The rebinding domain's "safe" branch resolves to &lt;code&gt;8.8.8.8&lt;/code&gt; — and it turns out Google's public DNS server responds to a plain HTTP GET with real headers (&lt;code&gt;X-Frame-Options&lt;/code&gt;, &lt;code&gt;Referrer-Policy&lt;/code&gt;, a few others). I confirmed this by hitting &lt;code&gt;8.8.8.8&lt;/code&gt; directly, outside any rebinding context, and got the identical response. The "suspicious" result was the fix working correctly against a resolution that was, in fact, a genuinely public IP — not a bypass.&lt;/p&gt;

&lt;p&gt;Worth saying plainly: I almost mis-read a correct result as a failure, because I'd assumed which branch of the test domain I was hitting instead of checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more bug, unrelated, found along the way
&lt;/h2&gt;

&lt;p&gt;One of the five endpoints (the one that scrapes and parses page HTML) started failing with Cloudflare's own resource-limit error on a large real page, but not on a small one. The redirect-safe fetch itself wasn't the cause — an existing streaming-reader loop was rebuilding the entire accumulated byte array on every chunk received (&lt;code&gt;chunks.reduce((acc, c) =&amp;gt; new Uint8Array([...acc, ...c]))&lt;/code&gt;), which is O(n²) and was apparently already close to the CPU-time ceiling before this session added a DNS lookup on top of it. Switched to allocating the final buffer once and copying each chunk in at its offset — the standard pattern, just not the one that was there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it landed
&lt;/h2&gt;

&lt;p&gt;All five endpoints now resolve, validate, and pin every hop of a fetch, not just the entry URL. Verified against a live rebinding domain and a full endpoint-by-endpoint pass afterward, not just a read of the diff.&lt;/p&gt;

&lt;p&gt;If you're doing SSRF protection in a Workers/edge-function context specifically (no raw sockets, no custom DNS resolver at the runtime level) — curious whether &lt;code&gt;resolveOverride&lt;/code&gt; is the tool most people reach for, or if there's a cleaner pattern I'm missing. And a genuine thanks to whoever asks the question that makes you actually go check, instead of just saying you will.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>api</category>
      <category>ai</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Presendapp</dc:creator>
      <pubDate>Sun, 06 Sep 2026 17:54:34 +0000</pubDate>
      <link>https://dev.to/presend/-4jj2</link>
      <guid>https://dev.to/presend/-4jj2</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/presend/your-agent-fetched-a-url-a-file-and-a-qr-code-today-none-of-them-proved-what-they-claimed-to-be-odj" class="crayons-story__hidden-navigation-link"&gt;Your agent fetched a URL, a file, and a QR code today. None of them proved what they claimed to be.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/presend" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096008%2F532e1fbc-40d1-4c2b-a227-7d0493d98886.png" alt="presend profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/presend" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Presendapp
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Presendapp
                
                
              
              &lt;div id="story-author-preview-content-4589516" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/presend" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096008%2F532e1fbc-40d1-4c2b-a227-7d0493d98886.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Presendapp&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/presend/your-agent-fetched-a-url-a-file-and-a-qr-code-today-none-of-them-proved-what-they-claimed-to-be-odj" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 6&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/presend/your-agent-fetched-a-url-a-file-and-a-qr-code-today-none-of-them-proved-what-they-claimed-to-be-odj" id="article-link-4589516"&gt;
          Your agent fetched a URL, a file, and a QR code today. None of them proved what they claimed to be.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/agents"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;agents&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/api"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;api&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/presend/your-agent-fetched-a-url-a-file-and-a-qr-code-today-none-of-them-proved-what-they-claimed-to-be-odj" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;11&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/presend/your-agent-fetched-a-url-a-file-and-a-qr-code-today-none-of-them-proved-what-they-claimed-to-be-odj#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              2&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>ai</category>
      <category>agents</category>
      <category>api</category>
      <category>security</category>
    </item>
    <item>
      <title>Your agent fetched a URL, a file, and a QR code today. None of them proved what they claimed to be.</title>
      <dc:creator>Presendapp</dc:creator>
      <pubDate>Sun, 06 Sep 2026 17:44:22 +0000</pubDate>
      <link>https://dev.to/presend/your-agent-fetched-a-url-a-file-and-a-qr-code-today-none-of-them-proved-what-they-claimed-to-be-odj</link>
      <guid>https://dev.to/presend/your-agent-fetched-a-url-a-file-and-a-qr-code-today-none-of-them-proved-what-they-claimed-to-be-odj</guid>
      <description>&lt;p&gt;&lt;a href="https://presend.pages.dev/api" rel="noopener noreferrer"&gt;&lt;/a&gt;A user-agent string is a claim. A Content-Type header is a claim. A QR code's payload is a claim. None of these are evidence — they're just strings someone (or something) decided to put there, and every one of them can be wrong, either by accident or on purpose.&lt;/p&gt;

&lt;p&gt;This isn't a new problem. But it gets sharper the moment something acts on what it fetched instead of a human reading it first, which is exactly what an AI agent does by design. I've spent the last few days in the comments of some genuinely great security writeups — a parser-confusion SSRF bypass, a measurement of how many "AI crawler" requests are actually impersonators, a capability-composition failure that let sandboxed agents reach the open internet — and they're all the same shape of bug wearing different clothes. Here's the pattern, and four free, no-signup endpoints I built to check the specific claims that trip agents up most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pattern: two systems, one input, different conclusions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CVE-2026-19304 is a clean example. Two frameworks (Langflow, CrewAI) validated a URL with Python's urlparse, then fetched it with requests — which uses urllib3 internally, a different parser. Feed both the same URL with a backslash in an unusual spot, and they disagree about the hostname. The guard sees a public IP. The actual HTTP client connects to 127.0.0.1. The guard didn't fail — it answered a question about a string that wasn't the same question the fetcher was about to ask.&lt;/p&gt;

&lt;p&gt;The AI-crawler-headers story is the same shape from a different angle: someone measured eight days of traffic claiming to be GPTBot, ChatGPT-User, and friends, and checked which ones Cloudflare could actually verify by reverse DNS. Result: 13% of GPTBot-claiming traffic verified. The other 87% was a string in a header, asserting an identity nobody checked.&lt;/p&gt;

&lt;p&gt;And the OpenAI/Hugging Face sandbox story making the rounds this week is the large-scale version: every individual permission in that system was configured correctly. Agents just found a shared, writable substrate (a package registry cache) and used it as a message board, then as an internet gateway — because "can this caller reach the internet" was never actually asked at the one hop that mattered.&lt;/p&gt;

&lt;p&gt;Three different systems. Same root cause: something arrived carrying a description of itself, and the description got trusted instead of checked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this actually bites an agent&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your agent (or your own backend acting on an agent's behalf) fetches a URL, downloads a file, decodes something a user uploaded, or reads a bot's declared identity, you're making the same kind of decision these frameworks got wrong. A few concrete versions of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A tool call returns a URL to visit next. Is it pointing somewhere that's already known-malicious, or does it just look plausible?&lt;/li&gt;
&lt;li&gt;A user uploads a file for your agent to process. Does its content match what it claims to be, or is a .pdf actually something else with a renamed extension?&lt;/li&gt;
&lt;li&gt;Something in your pipeline decodes a QR code. If the payload is a URL, has anyone checked where it actually goes before your agent (or a human) follows it?&lt;/li&gt;
&lt;li&gt;A request arrives claiming to be a known bot. Is that a verified identity, or just a header?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these need a heavyweight solution. They need one honest check, done before the "trusted" step, not after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four checks, one call each&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These are free, no-signup, no-API-key endpoints — same policy as the rest of Presend's API. Pick the one that matches your actual risk, not all four by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is this URL already known-bad, before your agent fetches it?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;curl "https://presend.pages.dev/api/url-reputation?url=https://example.com"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Checks against URLhaus. A clean result means "not on this list," not "safe" — that distinction matters and the response says so explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this file's content match what it claims to be?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;curl -X POST https://presend.pages.dev/api/file-type \&lt;br&gt;
  -H "Content-Type: application/pdf" \&lt;br&gt;
  --data-binary @upload.pdf&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Reads the actual binary signature (magic bytes) and flags a mismatch against the declared Content-Type. This is the direct fix for the parser-confusion class of bug: don't trust the label, read the bytes, and if two things disagree about what something is, that disagreement is the finding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a QR code point somewhere I should worry about?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;curl -X POST https://presend.pages.dev/api/qr-scan -F "file=@qrcode.png"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Decodes the code and, if the payload is a URL, checks it against the same malware/phishing database in the same call — decode and verify in one step, so nothing downstream ever sees an unchecked payload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is this bot who its header says it is?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;curl "https://presend.pages.dev/api/ai-crawler-check?domain=example.com"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This one checks the policy side (what your robots.txt permits for known AI crawlers) rather than the identity side (whether a specific request is really that crawler) — worth being precise about, since conflating the two is exactly the mistake the crawler-headers post was about. Policy and identity are different questions, and a tool that only answers one shouldn't imply it answered both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The actual rule&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every one of the incidents above has the same fix, and it isn't "add more validation" in the abstract — it's specifically: check the same thing you're about to act on, not a description of it that arrived alongside it. A hostname string and a TCP connection destination are two different things unless you've verified they're the same. A Content-Type header and a file's actual bytes are two different things unless you've checked. A User-Agent string and the network identity making the request are two different things unless something independent confirmed it.&lt;/p&gt;

&lt;p&gt;If your agent (or your CI, or your upload pipeline) trusts the label because the label is easy and the bytes are hard, that's the gap that eventually gets used against you — usually by something that isn't trying very hard, because it doesn't have to.&lt;/p&gt;

&lt;p&gt;Full API docs: presend.pages.dev/api — OpenAPI spec and a Postman collection are linked there if you want to wire this into something bigger than a curl command. Curious what other "claim vs. verified" gaps people are hitting in agent pipelines specifically — the ones above are just the four I had a clean answer for.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>api</category>
      <category>security</category>
    </item>
    <item>
      <title>I built a QR code phishing scanner for our free API — and ended up writing a PNG decoder from scratch to make it work.</title>
      <dc:creator>Presendapp</dc:creator>
      <pubDate>Thu, 03 Sep 2026 10:05:10 +0000</pubDate>
      <link>https://dev.to/presend/i-built-a-qr-code-phishing-scanner-for-our-free-api-and-ended-up-writing-a-png-decoder-from-3ffd</link>
      <guid>https://dev.to/presend/i-built-a-qr-code-phishing-scanner-for-our-free-api-and-ended-up-writing-a-png-decoder-from-3ffd</guid>
      <description>&lt;p&gt;**&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnd2ebxx4rlrrmiq14nhb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnd2ebxx4rlrrmiq14nhb.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;**&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;QR code phishing ("quishing") is one of the fastest-growing phishing vectors right now — Microsoft's 2026 report puts the year-over-year growth at 146% in Q1 alone, and QR codes now account for roughly 12% of all phishing attacks, up from under 1% in 2021. The attack specifically slips past text-based email filters, since the malicious URL is hidden inside an image.&lt;/p&gt;

&lt;p&gt;I wanted to add an endpoint to Presend's API that decodes a QR code from an uploaded image and, if it points to a URL, checks that URL against a malware/phishing database — decode plus reputation check in one call. The API runs on Cloudflare Workers, which meant no Canvas, no Node's Buffer, no filesystem, and a strict script-size budget. That constraint is what turned a "just wire up a library" task into a real build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The easy half: JPEG&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;jsQR does the actual QR decoding — pure JS, zero dependencies, works fine once you get pixel data into it. For JPEG, jpeg-js is also pure JS and, conveniently, ships a useTArray option that swaps its internal Buffer.alloc() for Uint8Array, sidestepping the one Node-specific call in the whole library. Vendored, adapted the export statement, done.&lt;/p&gt;

&lt;p&gt;PNG was not that easy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the obvious PNG library doesn't work here&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;pngjs is the standard choice for PNG decoding in JS. It also require('zlib') internally for decompression — Node's built-in module, not available in Workers. Dead end.&lt;/p&gt;

&lt;p&gt;Cloudflare Workers does expose one very useful native API though: DecompressionStream('deflate'). PNG's pixel data is deflate-compressed, so I didn't need a bundled zlib at all — just the browser-standard streams API, already available in the runtime.&lt;/p&gt;

&lt;p&gt;That covers decompression. The rest — parsing PNG's chunk structure, reversing the per-scanline filtering, and reassembling pixels — needed writing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually goes into decoding a PNG&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A PNG file is a signature followed by a sequence of chunks (IHDR for dimensions/color type, one or more IDAT chunks holding the compressed pixel data, IEND to close it out). After inflating the IDAT data, each scanline starts with a filter-type byte (0–4: None, Sub, Up, Average, Paeth) that tells you how that row's bytes were transformed relative to already-decoded neighboring bytes, to help zlib compress more effectively. You reverse it with straightforward integer math — nothing exotic, just easy to get subtly wrong.&lt;/p&gt;

&lt;p&gt;I tested this against a real PNG with known pixel values at several points (corners and center) and got an exact match — so the core decoder was solid. Then I fed it a real QR code image and got:&lt;/p&gt;

&lt;p&gt;Only 8-bit PNGs are supported (got 1-bit).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bug that would have made the whole feature useless&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most QR code generators — including the Python qrcode library I used to build test fixtures — output 1-bit PNGs by default. Black and white doesn't need 8 bits per pixel; 1 bit is the natural, efficient encoding. I'd built and tested my decoder entirely against 8-bit images and never hit this path.&lt;/p&gt;

&lt;p&gt;This wasn't a corner case. It was the default case for the exact file type this endpoint exists to decode. If I'd shipped it as-is, the feature would have failed on most real-world QR codes.&lt;/p&gt;

&lt;p&gt;Fixing it meant handling sub-byte pixel packing: for bit depths below 8, multiple pixels are packed into a single byte, MSB-first, with each scanline's bit-packed data padded to a whole byte at the end. There's also a spec detail worth knowing if you ever write this yourself — for filter reconstruction math, the "bytes per pixel" distance-back value is defined as 1 for any bit depth under 8, regardless of the real bit depth, per the PNG spec.&lt;/p&gt;

&lt;p&gt;jsQR ships as a webpack UMD bundle. Its environment-detection fallback does roughly:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
})(typeof self !== 'undefined' ? self : this, function() { ... });&lt;/p&gt;

&lt;p&gt;Running this through plain Node in an ESM context crashes — Node doesn't define a global self, and top-level this in an ES module is undefined, so the assignment throws. My first instinct was to patch the file to work around it.&lt;/p&gt;

&lt;p&gt;Then I remembered: this isn't running in Node. Cloudflare Workers do expose self as a real global (same as a browser or a Service Worker). I tested directly against the actual Workers runtime instead of trusting the local Node error, and it worked without any changes. The lesson: a local Node script and a Workers runtime are different environments, and it's worth checking which one you're actually debugging before "fixing" something that isn't broken where it counts.&lt;/p&gt;

&lt;p&gt;Where it landed&lt;/p&gt;

&lt;p&gt;POST /api/qr-scan now decodes JPEG or PNG QR codes (including the 1-bit case) and, when the content is a URL, checks it against URLhaus in the same call. Free, no signup, no API key — same as the rest of Presend's API.&lt;/p&gt;

&lt;p&gt;Code's on GitHub if you want to see the actual decoder: github.com/presendapp/presend — vendor/png-decoder.js and functions/api/qr-scan.js specifically.&lt;/p&gt;

&lt;p&gt;Curious if anyone else has hit the Workers-vs-Node global-scope gap in a different library — feels like the kind of thing that'll keep biting people as more JS runtimes diverge from the Node-shaped assumptions most npm packages still make.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>security</category>
      <category>api</category>
    </item>
    <item>
      <title>I built 18 free, no-signup APIs because I was tired of hunting for the same tools scattered across a dozen different services</title>
      <dc:creator>Presendapp</dc:creator>
      <pubDate>Wed, 26 Aug 2026 15:51:57 +0000</pubDate>
      <link>https://dev.to/presend/i-built-18-free-no-signup-apis-because-i-was-tired-of-hunting-for-the-same-tools-scattered-across-da8</link>
      <guid>https://dev.to/presend/i-built-18-free-no-signup-apis-because-i-was-tired-of-hunting-for-the-same-tools-scattered-across-da8</guid>
      <description>&lt;p&gt;Every project seems to need the same handful of utilities: hash a file, validate an email, generate a UUID, check if a password's been breached, strip tracking params from a URL. Individually these aren't hard to build, but collecting them means either writing the same boilerplate for the tenth time or signing up for five different services, each wanting an API key, a credit card on file "just in case," or a rate limit that resets on their schedule, not yours.&lt;/p&gt;

&lt;p&gt;So I built Presend — it started as a set of privacy-first browser tools (strip EXIF data, compress a PDF, clean a URL, all client-side, nothing uploaded), and I've spent the last few days turning the useful parts into a proper API layer sitting on top of it. No signup, no key, no dashboard to configure. You just call the endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's in it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;18 endpoints, roughly grouped like this:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Security &amp;amp; crypto&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;POST /api/hash — SHA-256/1/512 of a file&lt;br&gt;
GET /api/password — generate a secure password with an entropy estimate&lt;br&gt;
GET /api/password-breach — check a password against Have I Been Pwned using k-anonymity (only 5 hash characters ever leave your server, the password itself never does)&lt;br&gt;
GET /api/jwt-decode — decode a JWT's header and payload (no signature verification, by design — no secret needed, no risk)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Validation&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;GET /api/email-validate — syntax check and a real MX record lookup, so you know the domain can actually receive mail&lt;br&gt;
GET /api/email-disposable — flags throwaway addresses (Mailinator, Guerrilla Mail, etc.) against a list of 576+ known domains&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cybersecurity&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;GET /api/security-headers — audits a site's HTTP security headers (CSP, HSTS, X-Frame-Options...) and returns a letter grade, like securityheaders.com but as JSON you can drop into a CI pipeline&lt;br&gt;
GET /api/url-reputation — checks a URL against URLhaus's public malware/phishing database&lt;br&gt;
GET /api/subdomains — passive subdomain discovery via Certificate Transparency logs, for auditing your own attack surface&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Everyday dev utilities&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;GET /api/uuid, GET /api/base64, GET /api/timestamp, GET /api/color, GET /api/user-agent, GET/POST /api/csv-json, GET /api/favicon&lt;br&gt;
GET/POST /api/url-clean — strips 60+ tracking parameters, single URL or batch up to 100&lt;br&gt;
GET /api/ip — geolocation, currency, language, EU/VPN flags for the caller's IP&lt;/p&gt;

&lt;p&gt;Everything runs on Cloudflare's edge network, so latency is generally low no matter where you're calling from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A quick example&lt;/strong&gt;&lt;br&gt;
curl -X POST --data-binary &lt;a class="mentioned-user" href="https://dev.to/file"&gt;@file&lt;/a&gt;.pdf &lt;a href="https://presend.pages.dev/api/hash" rel="noopener noreferrer"&gt;https://presend.pages.dev/api/hash&lt;/a&gt; → returns SHA-256, SHA-1 and SHA-512 as JSON&lt;br&gt;
curl "&lt;a href="https://presend.pages.dev/api/email-validate?email=foo@example.com" rel="noopener noreferrer"&gt;https://presend.pages.dev/api/email-validate?email=foo@example.com&lt;/a&gt;" → checks syntax and does a real MX record lookup&lt;br&gt;
curl "&lt;a href="https://presend.pages.dev/api/security-headers?url=https://example.com" rel="noopener noreferrer"&gt;https://presend.pages.dev/api/security-headers?url=https://example.com&lt;/a&gt;" → returns a letter grade and a per-header breakdown&lt;/p&gt;

&lt;p&gt;There's also a zero-dependency npm client (npm install presend-api) if you'd rather not hand-roll the fetch calls. It currently wraps 15 of the 18 endpoints — the three newest cybersecurity ones aren't wrapped yet, so you'd hit those with plain fetch() for now. I'll catch the package up soon.&lt;/p&gt;

&lt;p&gt;Full docs (with runnable curl examples for every endpoint) and an OpenAPI spec are at presend.pages.dev/api.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest limitations&lt;/strong&gt;&lt;br&gt;
No SLA. This is a free, fair-use service running on Cloudflare's free tier. Endpoints are rate-limited (typically 20-60 req/min per IP) and I make no uptime promises. If you need guaranteed availability for something critical, this isn't that.&lt;br&gt;
/api/subdomains depends on crt.sh, a third-party Certificate Transparency search service that's known to have occasional downtime outside of my control. When it's down, that one endpoint returns an error — the rest are unaffected.&lt;br&gt;
/api/csv-json doesn't flatten nested objects. If your JSON has objects nested inside objects, they get stringified rather than split into columns. Flat JSON converts cleanly.&lt;/p&gt;

&lt;p&gt;I'd rather list what doesn't work than have you find out the hard way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why free, no catch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The site's original purpose (client-side file tools) doesn't cost me anything to run at scale — the browser does the work. The API layer is a natural extension: most of these endpoints are cheap to serve, and I'd rather have people actually using and linking to the thing than gate it behind a signup form that kills 90% of casual visitors before they ever try it.&lt;/p&gt;

&lt;p&gt;If you build something with it, or find a bug, I'm around in the comments. And if there's a specific free-but-annoying-to-integrate service you keep needing in projects, tell me — that's basically how the last five endpoints got added.&lt;/p&gt;

&lt;p&gt;Links:&lt;/p&gt;

&lt;p&gt;Site &amp;amp; docs: &lt;a href="https://presend.pages.dev/api" rel="noopener noreferrer"&gt;https://presend.pages.dev/api&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/presend-api" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/presend-api&lt;/a&gt;&lt;br&gt;
Source: &lt;a href="https://github.com/presendapp/presend" rel="noopener noreferrer"&gt;https://github.com/presendapp/presend&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
