<?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: ShipSafeScan</title>
    <description>The latest articles on DEV Community by ShipSafeScan (@shipsafescan).</description>
    <link>https://dev.to/shipsafescan</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%2F4096838%2F62ef35b1-88e3-42d8-8baf-95248175cd6a.png</url>
      <title>DEV Community: ShipSafeScan</title>
      <link>https://dev.to/shipsafescan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shipsafescan"/>
    <language>en</language>
    <item>
      <title>A 15-minute security pass for code your AI wrote</title>
      <dc:creator>ShipSafeScan</dc:creator>
      <pubDate>Sun, 30 Aug 2026 11:37:04 +0000</pubDate>
      <link>https://dev.to/shipsafescan/a-15-minute-security-pass-for-code-your-ai-wrote-8hk</link>
      <guid>https://dev.to/shipsafescan/a-15-minute-security-pass-for-code-your-ai-wrote-8hk</guid>
      <description>&lt;p&gt;When you're vibe-coding a side project, "working" and "safe to ship" feel like the same thing. They aren't. LLM-generated code is really good at making the happy path run, and really good at quietly skipping the boring parts that keep you out of trouble: auth checks, secret handling, dependency hygiene.&lt;/p&gt;

&lt;p&gt;I found this out going back through my own repos. I'd let an assistant scaffold a few apps, shipped them, and then went back to audit what I'd actually pushed. It wasn't pretty. Nothing exotic, just the same handful of mistakes over and over. So this is a field guide to those mistakes, with the exact commands I now run before I call something done. No tools required to follow along; everything here is grep, &lt;code&gt;npm&lt;/code&gt;, and reading your own diff.&lt;/p&gt;

&lt;p&gt;A quick caveat on how to read this: these are patterns I keep seeing in code that LLMs generate, described qualitatively from my own repos. I'm not putting a percentage on it. I don't have a representative sample, and neither does anyone who tells you "X% of AI code is insecure" without a citation. Treat it as a checklist, not a statistic.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Secrets that made it into git history
&lt;/h2&gt;

&lt;p&gt;The classic. You paste an API key into &lt;code&gt;.env&lt;/code&gt; to test something, later add &lt;code&gt;.env&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;, and feel safe. But if the file was ever committed &lt;em&gt;before&lt;/em&gt; you ignored it, the key is still sitting in your history forever.&lt;/p&gt;

&lt;p&gt;Check the working tree first:&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="c"&gt;# Obvious offenders in files you can see right now&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rEn&lt;/span&gt; &lt;span class="s2"&gt;"(api[_-]?key|secret|token|password|BEGIN.*PRIVATE KEY)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.{js,ts,jsx,tsx,py,env,json,yml,yaml}"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check history, because that's where keys hide:&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="c"&gt;# Was .env ever committed at any point?&lt;/span&gt;
git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--full-history&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"*.env"&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt;

&lt;span class="c"&gt;# Look for likely key shapes across all commits (example: AWS-style, generic hex)&lt;/span&gt;
git rev-list &lt;span class="nt"&gt;--all&lt;/span&gt; | xargs git &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s2"&gt;"AKIA[0-9A-Z]{16}|[a-f0-9]{32,}"&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you find anything: rotate the key first (assume it's burned), &lt;em&gt;then&lt;/em&gt; worry about scrubbing history. Rotating is the part that actually protects you. History rewrites (&lt;code&gt;git filter-repo&lt;/code&gt;) are secondary and can wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Secrets shipped to the browser
&lt;/h2&gt;

&lt;p&gt;Frontend frameworks expose any env var with a public prefix to the client bundle. In Next.js that's &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;; other frameworks have their own (&lt;code&gt;VITE_&lt;/code&gt;, &lt;code&gt;REACT_APP_&lt;/code&gt;, &lt;code&gt;PUBLIC_&lt;/code&gt;). An assistant wiring up a fetch call will happily reach for whatever variable is in scope, including a server key it should never expose.&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="c1"&gt;// This ships your key to every visitor's browser. Anyone can View Source.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&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;NEXT_PUBLIC_SERVICE_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&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 rule of thumb: a &lt;code&gt;PUBLIC&lt;/code&gt; prefix means "I am fine with the whole world reading this." Publishable/anon keys are designed for that. Service-role keys, private API keys, and anything that can write or read other users' data are not.&lt;/p&gt;

&lt;p&gt;Grep your client-side code for the prefix and eyeball every hit:&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"NEXT_PUBLIC_&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;VITE_&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;REACT_APP_"&lt;/span&gt; src/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"secret|service|admin|private"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything sensitive belongs behind a server route (an API route / server action), where the browser never sees the raw value.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. API and admin routes with no auth guard
&lt;/h2&gt;

&lt;p&gt;This is the one that bites hardest. The UI hides the "Delete everything" button behind a login screen, so it &lt;em&gt;feels&lt;/em&gt; protected. But the button just calls &lt;code&gt;/api/admin/reset&lt;/code&gt;, and that endpoint is a public URL. If the route handler doesn't check who's calling, anyone with the URL can call it directly with &lt;code&gt;curl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/admin/users/route.ts, looks fine, ships data to anyone&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// no auth check anywhere&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&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 fix is to check the session at the top of the handler, not just in the UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&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;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To find the gaps, list your routes and ask one question per file: &lt;em&gt;"If a stranger hit this URL with no cookie, what happens?"&lt;/em&gt;&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="c"&gt;# Every API route in a Next.js app-router project&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; ./node_modules &lt;span class="nt"&gt;-prune&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/api/*"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"route.*"&lt;/span&gt; &lt;span class="nt"&gt;-print&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then grep for the routes that &lt;em&gt;don't&lt;/em&gt; mention a session/auth helper, those are your suspects:&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rL&lt;/span&gt; &lt;span class="s2"&gt;"getSession&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;auth(&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;requireUser&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;verifyToken"&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/api/*"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"route.*"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Dependencies with known vulnerabilities
&lt;/h2&gt;

&lt;p&gt;LLMs are trained on a snapshot of the world, so they tend to reach for versions and packages that were popular &lt;em&gt;at training time&lt;/em&gt;, sometimes pinned to something with a known CVE. You don't have to guess; the tooling already knows.&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="c"&gt;# Ships with npm, no install needed&lt;/span&gt;
npm audit

&lt;span class="c"&gt;# Just the serious stuff, and a quick automated pass at safe upgrades&lt;/span&gt;
npm audit &lt;span class="nt"&gt;--audit-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;high
npm audit fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want an ecosystem-agnostic view (or you're on pnpm/yarn and want a second opinion), OSV-Scanner reads your lockfile against the open OSV vulnerability database:&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="c"&gt;# https://github.com/google/osv-scanner&lt;/span&gt;
osv-scanner &lt;span class="nt"&gt;--lockfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;package-lock.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two habits worth building: don't blind-run &lt;code&gt;npm audit fix --force&lt;/code&gt; (it can yank in breaking major versions), and re-run the audit on a schedule, because new CVEs get published against dependencies you already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. CORS, rate limits, and errors that overshare
&lt;/h2&gt;

&lt;p&gt;The quiet trio. None of these throws an error in dev, so they sail straight to prod.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CORS set to a wildcard&lt;/strong&gt; lets any website call your API from a user's browser:&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="c1"&gt;// Reflects "allow everyone", fine for a truly public read-only API,&lt;/span&gt;
&lt;span class="c1"&gt;// a problem the moment the endpoint does anything user-specific.&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scope it to the origins you actually serve instead of &lt;code&gt;*&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No rate limiting&lt;/strong&gt; means a single script can hammer your login route, your signup, or your LLM-backed endpoint (that last one can also run up a real bill). Even a minimal per-IP limit in front of sensitive routes changes the economics for an attacker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors that leak internals&lt;/strong&gt; hand attackers a map. Returning the raw exception is convenient in dev and a gift in prod:&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="c1"&gt;// Don't send this to the client in production&lt;/span&gt;
&lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log the full error server-side; return a generic message and a request ID to the client. Grep for the tell:&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"err.stack&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;error.message&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;console.log(err"&lt;/span&gt; src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The actual 5-minute / 15-minute pass
&lt;/h2&gt;

&lt;p&gt;Here's the routine distilled. You can literally paste these in order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5-minute triage&lt;/strong&gt; (do this every time before you push):&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="c"&gt;# 1. Secrets in the working tree&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rEn&lt;/span&gt; &lt;span class="s2"&gt;"(api[_-]?key|secret|token|password)"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.{js,ts,py,env}"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node_modules
&lt;span class="c"&gt;# 2. Was .env ever committed?&lt;/span&gt;
git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"*.env"&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt;
&lt;span class="c"&gt;# 3. Public-prefixed secrets leaking to the client&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"NEXT_PUBLIC_&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;VITE_&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;REACT_APP_"&lt;/span&gt; src/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"secret|service|admin"&lt;/span&gt;
&lt;span class="c"&gt;# 4. Known-vulnerable deps&lt;/span&gt;
npm audit &lt;span class="nt"&gt;--audit-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;15-minute pass&lt;/strong&gt; (do this before anything goes to real users):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Walk every file under &lt;code&gt;**/api/**&lt;/code&gt; and answer &lt;em&gt;"stranger, no cookie, what happens?"&lt;/em&gt; for each one. Add the &lt;code&gt;401/403&lt;/code&gt; guard where the answer is "they get data."&lt;/li&gt;
&lt;li&gt;Confirm no &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; on endpoints that return user-specific data.&lt;/li&gt;
&lt;li&gt;Put a basic rate limit in front of auth and any LLM/paid endpoint.&lt;/li&gt;
&lt;li&gt;Grep for &lt;code&gt;err.stack&lt;/code&gt; / raw error returns and swap them for generic messages + server-side logging.&lt;/li&gt;
&lt;li&gt;Rotate any key you found in steps 1-3. Assume anything committed is compromised.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole thing. It's not sophisticated, and that's the point, the failures in AI-generated code are usually boring omissions, so a boring checklist catches most of them.&lt;/p&gt;




&lt;p&gt;I ended up running this so often across my own projects that I got tired of doing it by hand, so I wrapped the repetitive parts (the secret grep, the dependency check, the "is this route guarded" pass) into a small scanner you point at a public GitHub repo: &lt;a href="https://shipsafescan.com" rel="noopener noreferrer"&gt;ShipSafeScan&lt;/a&gt;. It won't catch everything a careful read will, nothing does, but it's a fast first pass. Either way, the checklist above works on its own. Run it before you ship.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
