<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[DeployVet notes]]></title><description><![CDATA[DeployVet notes]]></description><link>https://deployvetnotes.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 30 Oct 2026 21:55:39 GMT</lastBuildDate><atom:link href="https://deployvetnotes.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What about 630 website checks showed me in the first week, and what they cannot show]]></title><description><![CDATA[I build DeployVet, a free tool that does a website security and bug check from the outside and grades the site A to F. I wanted to share what the numbers…]]></description><link>https://deployvetnotes.hashnode.dev/what-about-630-website-checks-showed-me-in-the-first-week-and-what-they-cannot-show</link><guid isPermaLink="true">https://deployvetnotes.hashnode.dev/what-about-630-website-checks-showed-me-in-the-first-week-and-what-they-cannot-show</guid><category><![CDATA[Security]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Hayden Young]]></dc:creator><pubDate>Thu, 29 Oct 2026 00:35:36 GMT</pubDate><content:encoded><![CDATA[<p>I build DeployVet, a free tool that does a website security and bug check from the outside and grades the site A to F. I wanted to share what the numbers look like after the first week, and be clear about what they do not prove.</p>
<h2>The short version</h2>
<ul>
<li>Oct 20 to 26, 2026: 645 checks on 633 unique domains (our own test domains excluded).</li>
<li>Using each domain's latest grade: 260 domains (about 41%) scored A or B, and 372 (about 59%) scored C, D or F. One had no grade.</li>
<li>The public counter read 650 total checks as of Mon Oct 26, 2026, about 11:55 AM Central.</li>
<li>On the public board, the 20 lowest results were 4 F, 9 D and 7 C. The 20 highest were all A (19 scored 100, one scored 95).</li>
</ul>
<p>These sites are ones people chose to type in, not a random sample. Read the method notes before you quote any of this.</p>
<h2>Grade tables</h2>
<p>All domains checked, Oct 20 to 26, 2026 (latest grade per domain, 633 domains):</p>
<table>
<thead>
<tr>
<th>Grade group</th>
<th>Domains</th>
<th>Share of 633</th>
</tr>
</thead>
<tbody><tr>
<td>A or B</td>
<td>260</td>
<td>about 41%</td>
</tr>
<tr>
<td>C, D or F</td>
<td>372</td>
<td>about 59%</td>
</tr>
<tr>
<td>No grade</td>
<td>1</td>
<td>under 1%</td>
</tr>
</tbody></table>
<p>The query reports A/B and C/D/F as groups, so I am not giving a count for each single letter.</p>
<p>Newest 10 on the public board:</p>
<table>
<thead>
<tr>
<th>Grade</th>
<th>Count</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>5</td>
</tr>
<tr>
<td>B</td>
<td>2</td>
</tr>
<tr>
<td>C</td>
<td>1</td>
</tr>
<tr>
<td>D</td>
<td>2</td>
</tr>
<tr>
<td>F</td>
<td>0</td>
</tr>
</tbody></table>
<p>Scores in this slice run from 46 to 100.</p>
<p>20 lowest on the board:</p>
<table>
<thead>
<tr>
<th>Grade</th>
<th>Count</th>
<th>Score range</th>
</tr>
</thead>
<tbody><tr>
<td>F</td>
<td>4</td>
<td>11 to 39</td>
</tr>
<tr>
<td>D</td>
<td>9</td>
<td>46 to 56</td>
</tr>
<tr>
<td>C</td>
<td>7</td>
<td>61 to 71</td>
</tr>
</tbody></table>
<p>20 highest on the board:</p>
<table>
<thead>
<tr>
<th>Grade</th>
<th>Count</th>
<th>Score range</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>20</td>
<td>95 to 100</td>
</tr>
</tbody></table>
<p>The board tables are a small, opt-in slice. Do not read them as the overall picture; the first table is the overall one.</p>
<h2>How grading works</h2>
<ul>
<li>A is 90 and up, B is 75 and up, C is 60 and up, D is 40 and up, and F is below 40.</li>
<li>Each problem takes points off based on how serious it is. A serious problem also caps the grade. One critical finding keeps a site at F. One high-severity finding keeps it at D or below.</li>
<li>The free check shows only the worst problem it found, with a plain-English fix. It does not list everything.</li>
</ul>
<h2>What the checker looks at</h2>
<p>It only reads what any visitor can already see. It does not log in, guess passwords or try to break anything, and it follows robots.txt.</p>
<ul>
<li>Does the padlock (HTTPS) work, and does http redirect to https?</li>
<li>Are the usual browser safety settings (security headers) set?</li>
<li>Are private files like /.env or other sensitive paths reachable from the outside?</li>
<li>Do the pages load?</li>
</ul>
<p>This is not a penetration test. It cannot see behind a login and it cannot judge your business logic.</p>
<h2>Method notes and limits</h2>
<ul>
<li>Source: a read-only query of the free check results table in our database.</li>
<li>Window: Oct 20 to 26, 2026.</li>
<li>Latest grade per domain: if a domain was checked more than once, only its most recent grade is counted in the A/B vs C/D/F split. The 645 figure counts every check, including repeats.</li>
<li>Exclusions: our own test domains, deployvet.com and example.com. Some other test-looking domains may remain in the data, so treat the counts as approximate.</li>
<li>Not a random sample. These are sites people typed in themselves. Sites whose owners care about security, or who suspect a problem, may be more likely to run it, and I cannot tell which way that pushes the numbers.</li>
<li>The 650 total on the public counter and the 645 in the query are different counts (different filters and time of day). I am using the query for the split and the counter only as a running total.</li>
<li>The public board shows only 50 rows at a time (20 highest, 20 lowest, 10 newest), opt-in, with masked domains. I cannot tell you what kinds of sites they were, and I will not name anyone.</li>
<li>Not every site checked was made with an AI or no-code tool. I do not have a clean way to tell, so I will not make claims about "AI-built sites" as a group.</li>
<li>Scores can change as the scanner changes. A grade is a snapshot from the day it was run.</li>
<li>Automated checks make mistakes, both missing things and flagging things that are fine.</li>
</ul>
<h2>What I do not know yet</h2>
<p>I do not have a verified breakdown of which problems come up most often, so I am not going to guess at one. If I get that checked properly, I will post it.</p>
<h2>Your turn</h2>
<p>If you have shipped something built with an AI tool, what is the last thing you check before you share the link?</p>
<p>I build DeployVet. If you want to try the free check: <a href="https://deployvet.com/?utm_source=hashnode&amp;utm_campaign=datapost">https://deployvet.com/?utm_source=hashnode&amp;utm_campaign=datapost</a></p>
]]></content:encoded></item><item><title><![CDATA[How to check your website's security headers in 5 minutes (and turn on the 4 that matter)]]></title><description><![CDATA[Every time someone visits your website, your server sends a few lines of instructions along with the page. They're called response headers. Some of them…]]></description><link>https://deployvetnotes.hashnode.dev/how-to-check-your-website-s-security-headers-in-5-minutes-and-turn-on-the-4-that-matter</link><guid isPermaLink="true">https://deployvetnotes.hashnode.dev/how-to-check-your-website-s-security-headers-in-5-minutes-and-turn-on-the-4-that-matter</guid><category><![CDATA[Security]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Hayden Young]]></dc:creator><pubDate>Mon, 26 Oct 2026 13:45:27 GMT</pubDate><content:encoded><![CDATA[<p>Every time someone visits your website, your server sends a few lines of instructions along with the page. They're called response headers. Some of them tell the browser how careful to be: always use the secure connection, don't let other sites wrap your page in a frame, don't guess file types.</p>
<p>They cost nothing. They take minutes to add. And a lot of sites, from hand-built WordPress sites to apps made with Lovable or Bolt, ship without them.</p>
<p>Here's how to check yours and fix the gaps.</p>
<h2>Step 1: Look at your headers (2 minutes)</h2>
<p><strong>Option A: your browser.</strong> Open your site in Chrome or Firefox. Press F12 (or right-click and choose Inspect). Click the <strong>Network</strong> tab, then reload the page. Click the first row, which is your page itself. Scroll to <strong>Response Headers</strong>.</p>
<p><strong>Option B: one command.</strong> If you have a terminal:</p>
<pre><code>curl -sI https://yoursite.com
</code></pre>
<p>The <code>-I</code> asks for headers only. You'll get a short list back.</p>
<p>Now look for these four names:</p>
<ol>
<li><code>strict-transport-security</code></li>
<li><code>x-content-type-options</code></li>
<li><code>x-frame-options</code> (or <code>frame-ancestors</code> inside <code>content-security-policy</code>)</li>
<li><code>referrer-policy</code></li>
</ol>
<p>Write down which ones are missing.</p>
<h2>Step 2: Know what each one does (1 minute)</h2>
<p><strong>Strict-Transport-Security (HSTS).</strong> Tells the browser: "Only ever talk to me over https." Without it, a visitor who types your address without https can land on the plain, unprotected version first. A common value:</p>
<pre><code>Strict-Transport-Security: max-age=31536000
</code></pre>
<p>Only add this once https works everywhere on your site, including subdomains you plan to cover. Browsers will remember it for the full max-age.</p>
<p><strong>X-Content-Type-Options.</strong> Stops the browser from guessing what a file is. One value, always the same:</p>
<pre><code>X-Content-Type-Options: nosniff
</code></pre>
<p><strong>X-Frame-Options.</strong> Stops other sites from loading your page inside a hidden frame and tricking people into clicking things (this is called clickjacking):</p>
<pre><code>X-Frame-Options: DENY
</code></pre>
<p>If you embed your own pages in frames, use <code>SAMEORIGIN</code> instead.</p>
<p><strong>Referrer-Policy.</strong> Controls how much of your page address gets passed along when someone clicks a link off your site. Useful if your URLs ever hold things like reset tokens or order numbers:</p>
<pre><code>Referrer-Policy: strict-origin-when-cross-origin
</code></pre>
<h2>Step 3: Turn them on (2 minutes, depends on your host)</h2>
<p><strong>Vercel.</strong> Add a <code>vercel.json</code> in your project root:</p>
<pre><code class="language-json">{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Strict-Transport-Security", "value": "max-age=31536000" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
      ]
    }
  ]
}
</code></pre>
<p><strong>Netlify.</strong> Add a file named <code>_headers</code> to your publish folder:</p>
<pre><code>/*
  Strict-Transport-Security: max-age=31536000
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
</code></pre>
<p><strong>Cloudflare in front of your site.</strong> Use Rules, then Transform Rules, then "Modify Response Header," and add each one. HSTS has its own switch under SSL/TLS, then Edge Certificates.</p>
<p><strong>WordPress.</strong> Many hosts have a security headers setting in their control panel. If yours doesn't, a well-known security plugin can add them. Pick one plugin, not three.</p>
<p><strong>Wix, Squarespace, Shopify.</strong> You usually can't edit headers yourself. The platform sets them. Check what you have with Step 1. If something important is missing, ask their support.</p>
<h2>Step 4: Check again</h2>
<p>Reload with F12 open, or run the <code>curl</code> command again. All four should show up. Then click around your site for a minute. If something broke (usually an embedded video or a payment widget inside a frame), that's almost always X-Frame-Options. Switch it to <code>SAMEORIGIN</code> or allow that one page.</p>
<h2>What about Content-Security-Policy?</h2>
<p>CSP is the strongest header and the easiest to break your site with. It lists exactly where scripts, images and styles may load from. Start with <code>Content-Security-Policy-Report-Only</code> so the browser reports problems without blocking anything. Fix what it reports, then switch to the real header. Give it its own afternoon.</p>
<h2>The honest part</h2>
<p>Headers are seatbelts, not a lock. They won't fix a database that anyone can read or a secret key sitting in your front-end code. But they're the cheapest fix on this list, and the first thing a stranger can see.</p>
<p>If you'd rather not do Step 1 by hand, DeployVet runs a free outside check of your site. It looks at headers, the padlock, exposed files and broken pages, and explains what it finds in plain English: <a href="https://deployvet.com/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=howto">https://deployvet.com/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=howto</a></p>
<p>It reads what's public, the same way a stranger would. It isn't a penetration test.</p>
]]></content:encoded></item><item><title><![CDATA[Test your own Supabase app like a stranger: a 15-minute walkthrough]]></title><description><![CDATA[Three tests, four SQL lines and one curl command that show what your public key can really do]]></description><link>https://deployvetnotes.hashnode.dev/test-your-own-supabase-app-like-a-stranger-a-15-minute-walkthrough</link><guid isPermaLink="true">https://deployvetnotes.hashnode.dev/test-your-own-supabase-app-like-a-stranger-a-15-minute-walkthrough</guid><category><![CDATA[supabase]]></category><category><![CDATA[Security]]></category><category><![CDATA[postgres]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Hayden Young]]></dc:creator><pubDate>Sun, 25 Oct 2026 12:32:16 GMT</pubDate><content:encoded><![CDATA[<p>Many publicized Supabase leaks are not clever attacks. They are someone with the project's public key asking the database for rows, and the database saying yes. You can run that same experiment on your own app in about fifteen minutes. Here is how.</p>
<h2>What is public and what is not</h2>
<p>Your project ships two keys. The <code>anon</code> key is designed to sit in your front end. The <code>service_role</code> key bypasses Row Level Security completely and must only ever live on a server. If you used an AI builder, the first thing to confirm is that nothing named <code>service_role</code> appears in your deployed JavaScript, and that no secret sits behind a <code>VITE_</code> or <code>NEXT_PUBLIC_</code> prefix.</p>
<p>Because the anon key is public, your safety rests entirely on RLS and grants. So test those.</p>
<h2>Test 1: which tables have RLS off?</h2>
<pre><code class="language-sql">select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;
</code></pre>
<p>Any row with <code>rowsecurity = false</code> is open to anyone with the anon key. Tables created through the dashboard editor usually get RLS on by default. Tables created by SQL, which is what AI tools often do, do not unless the SQL says so.</p>
<h2>Test 2: ask the API as a stranger</h2>
<p>Open a terminal and run this with your own project URL and anon key:</p>
<pre><code class="language-bash">curl "https://YOUR-REF.supabase.co/rest/v1/profiles?select=*&amp;limit=5" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"
</code></pre>
<p>Repeat for each table. You want an empty list or a permission error. If real rows come back, a stranger can read them too. Try the same with a POST and a DELETE only on a throwaway test table, never on production data.</p>
<h2>Test 3: two accounts, one row</h2>
<p>RLS that passes Test 2 can still leak between users. Create accounts A and B. Insert a row as A, note its id, then request that id while logged in as B. If B gets it, your policy checks "authenticated" rather than ownership. The fix is a policy like:</p>
<pre><code class="language-sql">create policy "owner reads own rows" on notes
for select to authenticated
using (auth.uid() = user_id);
</code></pre>
<p>Write separate policies for insert, update and delete, and add <code>with check</code> clauses so users cannot write rows that belong to someone else.</p>
<h2>Views, storage and grants</h2>
<p>Three places where people get caught after RLS looks fine:</p>
<ul>
<li><strong>Views.</strong> A view runs with its owner's rights by default, which usually skips RLS. On Postgres 15 and later, create it with <code>security_invoker = true</code>, or alter existing views.</li>
<li><strong>Storage.</strong> A policy that says "authenticated users can read bucket X" lets every signed-in user open everyone's files. Store files under a folder named for the user's id and check the folder name in the policy.</li>
<li><strong>Grants.</strong> If an AI "fixes" a <code>permission denied</code> error by turning RLS off or granting everything to <code>anon</code>, undo it. Grant each role only what it needs.</li>
</ul>
<p>Supabase has said that new tables in the public schema will stop being exposed to the Data API automatically from October 30, 2026. Check their changelog for the exact details. If you hit error <code>42501</code> afterwards, add the specific grant and keep RLS on in the same migration.</p>
<h2>If a key already leaked</h2>
<p>Rotate it in the dashboard, update your server environment, redeploy, and check your Git history as well as the live bundle. Deleting a key from the current code does not remove it from old commits.</p>
<h2>Where this stops</h2>
<p>These tests only show what a stranger with your public key can do. They do not cover business logic, payment flows or anything behind a login you have not tried by hand. Think of them as a first pass, not a penetration test.</p>
<p>I built <a href="https://deployvet.com/?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=firstcustomer">DeployVet</a> partly to automate Tests 1 and 2 from the outside, along with exposed files, headers and broken pages. It is free to try and it only looks at the live site, so it will not see Test 3. Run that one yourself.</p>
<p>Fifteen minutes now beats writing the apology email later.</p>
]]></content:encoded></item></channel></rss>