<?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: Sukanya Mondol</title>
    <description>The latest articles on DEV Community by Sukanya Mondol (@sukanyamondol).</description>
    <link>https://dev.to/sukanyamondol</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%2F4196141%2F0c8a4a99-0b65-4cce-a6c1-2173df9e0e73.png</url>
      <title>DEV Community: Sukanya Mondol</title>
      <link>https://dev.to/sukanyamondol</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sukanyamondol"/>
    <language>en</language>
    <item>
      <title>AstraOS: I built a Linux distribution with an AI assistant that asks before it touches anything</title>
      <dc:creator>Sukanya Mondol</dc:creator>
      <pubDate>Tue, 20 Oct 2026 14:08:19 +0000</pubDate>
      <link>https://dev.to/sukanyamondol/astraos-i-built-a-linux-distribution-with-an-ai-assistant-that-asks-before-it-touches-anything-1m3o</link>
      <guid>https://dev.to/sukanyamondol/astraos-i-built-a-linux-distribution-with-an-ai-assistant-that-asks-before-it-touches-anything-1m3o</guid>
      <description>&lt;p&gt;AstraOS is a Linux distribution built from Debian packages, with its own package selection, desktop configuration, branding, installer configuration and build pipeline. Its ISO builds in GitHub Actions, passes the checks that build runs, and boots in VirtualBox. It is also an alpha: version 0.2.0, codename Nova, x86_64 only, not finished, and nothing in it has ever run on physical hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it exists
&lt;/h2&gt;

&lt;p&gt;The founding brief was blunt about what it did not want: "I do not want a website pretending to be an operating system. I want an actual, bootable, installable Linux distribution that can eventually run on physical computers."&lt;/p&gt;

&lt;p&gt;The roadmap gives the reason. AstraOS should be the operating system for the PC people already own: a private, local-first computer that turns plain-language instructions into controlled, verifiable work. Windows 10 support ended in October 2025 and many working PCs cannot officially run Windows 11. The "AI distributions" that get talked about route agent work to cloud services by default, and cloud agent products have drawn privacy criticism for reading and writing personal folders.&lt;/p&gt;

&lt;p&gt;The hardware target follows. The reference machine is an Intel Core i5-2500S, a 2011 Sandy Bridge chip, and the assistant is sized for that class on purpose. One principle drives the rest: keep the system usable on modest hardware, which is why the desktop chrome is flat and the expensive graphics live on the boot splash.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually in it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Base.&lt;/strong&gt; Debian 13 (trixie), pinned in one file and checked against the live archive before a build, using Debian's own &lt;code&gt;linux-image-amd64&lt;/code&gt; kernel (6.12 series) with systemd as the init system. It is a derivative, not a new kernel and not an official Debian release. Packages are &lt;code&gt;.deb&lt;/code&gt; with APT and dpkg, selected from ten reviewed lists under &lt;code&gt;config/package-lists&lt;/code&gt;, and the root filesystem is ext4 once installed and squashfs on the live image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Desktop.&lt;/strong&gt; Xfce on X11, configured and branded: its own wallpaper, panel, icon theme, cursor, LightDM greeter, Plymouth splash and GRUB menu. The identity has two layers. The brand layer, a neon ring with gradients and bloom, appears where you see the system briefly: boot, login, installer. The desktop layer is flat graphite and cyan with no transparency, because a blur pass on every panel repaint is what makes a desktop feel slow on old hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Installer.&lt;/strong&gt; Calamares, configured and branded. The target disk is never assumed and erasing is never the default, and the build refuses to produce an image that breaks that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target hardware.&lt;/strong&gt; x86_64 Intel or AMD from roughly 2008 onwards. VirtualBox is the primary target, and the guest agents for QEMU/KVM and VMware ship in the image. Graphics drivers are open-source only, so NVIDIA goes through nouveau.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovery.&lt;/strong&gt; A failsafe GRUB entry that drops the splash and turns off modesetting and ACPI, so a failure is visible instead of hidden. &lt;code&gt;astraos-recovery-guide&lt;/code&gt; prints the guidance on a running system, and &lt;code&gt;docs/RECOVERY.md&lt;/code&gt; covers the cases that need the medium. The snapshot, break, restore drill with Timeshift has not been performed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The assistant
&lt;/h2&gt;

&lt;p&gt;The interesting part of this project is not the model. It is what happens between the model's request and the machine.&lt;/p&gt;

&lt;p&gt;The assistant runs entirely on the computer: a small Python server using only the standard library, and one HTML file with the styles and script inline. A test fails if an external address appears in that page. There is no cloud provider to configure and no API key to enter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The repertoire is closed.&lt;/strong&gt; The model may ask for one of 32 named actions and nothing else. The plan is re-checked against that catalogue before it runs, an unknown action name is refused rather than guessed at, and llama.cpp constrains generation with a JSON schema, so a tool call is valid or empty, never half-written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The confirmation set is seven actions.&lt;/strong&gt; &lt;code&gt;run_command&lt;/code&gt;, &lt;code&gt;shutdown&lt;/code&gt;, &lt;code&gt;reboot&lt;/code&gt;, &lt;code&gt;suspend&lt;/code&gt;, &lt;code&gt;close_window&lt;/code&gt;, &lt;code&gt;type_text&lt;/code&gt; and &lt;code&gt;press_key&lt;/code&gt;. Each has a question written for it, because a person cannot consent to a category of thing. The page shows "the assistant wants to run this command (command='df -h')", not "allow the assistant to run a command". The set grew by one when the research tools landed: &lt;code&gt;write_file&lt;/code&gt; writes into the assistant's own workspace and asks as well, so the catalogue asks before eight actions today while the README still says seven.&lt;/p&gt;

&lt;p&gt;Underneath sit four permission levels: &lt;code&gt;observe&lt;/code&gt; reads and changes nothing, &lt;code&gt;prepare&lt;/code&gt; changes the session's surroundings reversibly, &lt;code&gt;modify&lt;/code&gt; can change or lose your work (typing, key presses, closing a window, writing a file), and &lt;code&gt;privileged&lt;/code&gt; changes the machine (suspend, reboot, shutdown, running a command). Everything above &lt;code&gt;prepare&lt;/code&gt; is denied until a human says yes, and the approval names the resolved arguments and the effect rather than the category.&lt;/p&gt;

&lt;p&gt;The reasoning matters more than the list. The model cannot approve itself: the plan comes from the model, the yes comes from a person in front of the screen, and the web page and the command line both go through one enforcement point, so neither can take a different route around it. A denial is not a sentence in a message. It is the action not running.&lt;/p&gt;

&lt;p&gt;One decision I got wrong and reversed. I had argued that asking for every typed phrase would make typing useless, so &lt;code&gt;type_text&lt;/code&gt; and &lt;code&gt;press_key&lt;/code&gt; were left out of the set. That was wrong, because a terminal turns typing into a way to run a command. They are in the set now, and the person is asked once per plan with the exact text, not once per phrase.&lt;/p&gt;

&lt;p&gt;Documents are data. When the assistant reads a file containing instruction-shaped text, the request is marked as coming from a document and the guard refuses everything above &lt;code&gt;observe&lt;/code&gt; for it. The test fixtures order the assistant to open an exfiltration URL, run a command and type over a document. Each one is refused, and the refusal is recorded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sandbox covers the model server, not the actions.&lt;/strong&gt; With bubblewrap or firejail installed, &lt;code&gt;llama-server&lt;/code&gt; runs with the root and the model folder read-only and a private &lt;code&gt;/tmp&lt;/code&gt; and &lt;code&gt;/dev/shm&lt;/code&gt;. The network stays shared on purpose, because the page reaches the server over &lt;code&gt;127.0.0.1&lt;/code&gt;. Landlock is not used. The actions run as you, with your rights, and are not sandboxed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Undo is a tested library, not yet a feature.&lt;/strong&gt; It copies files into a snapshot directory with a manifest, restores them exactly, and removes paths that did not exist before. What is missing is the wiring: no privileged action takes a snapshot yet, and there is no &lt;code&gt;astra-ai undo&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The budgets exist so a weak model cannot loop forever.&lt;/strong&gt; A plan longer than twelve steps is refused before it starts. A task that spends its 900-second budget is filed &lt;code&gt;paused&lt;/code&gt; where it stopped rather than &lt;code&gt;failed&lt;/code&gt;, and &lt;code&gt;astra-ai resume&lt;/code&gt; continues it. A task may make at most 100 tool calls. A memory cap exists as &lt;code&gt;RLIMIT_AS&lt;/code&gt; on the model server and is off by default.&lt;/p&gt;

&lt;p&gt;Every decision is recorded as a JSON line carrying the SHA-256 of the line before it, so &lt;code&gt;astra-guard verify&lt;/code&gt; can name the first record that was edited or removed. The chain is tamper-evident, not signed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The engines are built in CI.&lt;/strong&gt; Compiling llama.cpp costs about twenty minutes of every core, so the Build AI engine workflow builds it for two variants: &lt;code&gt;avx&lt;/code&gt; for the Sandy Bridge class and &lt;code&gt;avx2&lt;/code&gt; for Haswell and later. The avx build is proven on an emulated Sandy Bridge with a real model answering over HTTP, because that class of machine once died with SIGILL and printed nothing. The avx2 build is proven natively, with a disassembly check that the 256-bit code is really there. No variant is ever compiled for the build machine's own instruction set. Both are published as the &lt;code&gt;ai-engine-2026-10-19&lt;/code&gt; release, and the image build downloads those assets, verifies their SHA-256 and installs both variants.&lt;/p&gt;

&lt;p&gt;The executor is a 1.7B model that reads one request and makes one tool call; the middle brain is a 4B model that writes the plan. The split follows the measured finding that multi-agent systems on small models are planner-limited rather than executor-limited, so the parameter budget goes into the planner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Build it yourself on a Debian 13 host with about 25 GB of disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/rafikulislammollaq-dev/astraos.git
&lt;span class="nb"&gt;cd &lt;/span&gt;astraos
make bootstrap     &lt;span class="c"&gt;# install the build dependencies&lt;/span&gt;
make iso           &lt;span class="c"&gt;# build the image into dist/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;make check&lt;/code&gt; runs the static suite, &lt;code&gt;make lint&lt;/code&gt; checks shell syntax, and &lt;code&gt;make validate-iso&lt;/code&gt;, &lt;code&gt;make checksums&lt;/code&gt; and &lt;code&gt;make manifest&lt;/code&gt; work on the image in &lt;code&gt;dist/&lt;/code&gt;. Or run the Build ISO workflow from the Actions tab and take the &lt;code&gt;astraos-iso&lt;/code&gt; artifact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In VirtualBox.&lt;/strong&gt; Verify the download 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="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; astraos-0.2.0-alpha-amd64.iso.sha256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect &lt;code&gt;astraos-0.2.0-alpha-amd64.iso: OK&lt;/code&gt;. If it does not, stop.&lt;/p&gt;

&lt;p&gt;Create the VM: name it &lt;code&gt;AstraOS 0.2.0 Alpha&lt;/code&gt;, point it at the verified ISO, choose Type Linux and Version Debian (64-bit), decline VirtualBox's unattended installation, and give it 4096 MB of memory, 2 processors and a 30 GB dynamically allocated VDI disk. The installer's minimum is 2048 MB and 8 GB. For firmware, tick Enable EFI under Settings, System, Motherboard for UEFI, or leave it unticked for legacy BIOS.&lt;/p&gt;

&lt;p&gt;Start it, wait for the GRUB menu and choose the normal entry. The live user is &lt;code&gt;astra&lt;/code&gt; with no password, and the live session is not persistent.&lt;/p&gt;

&lt;p&gt;To install, launch "Install AstraOS" from the menu or the dock and authenticate through the polkit prompt. On partitioning, choose Erase disk, because the disk is virtual, and check that the device you are erasing is the virtual one. Create a user with a password, read the summary, confirm, and choose Restart now when the queue finishes. Before booting the installed system, remove the ISO from the optical drive. That is the step people skip, and then they conclude the installer failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On physical hardware, the documentation says do not do this yet.&lt;/strong&gt; AstraOS 0.2.0 Alpha has never been booted on physical hardware, the installer has never been run there, and no hardware has been tested. The procedure is written in advance so it is not improvised afterwards: verify the checksum, back up everything you cannot lose, write the image to a USB drive with&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;sudo dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;astraos-0.2.0-alpha-amd64.iso &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/sdX &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4M &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress &lt;span class="nv"&gt;oflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which erases that drive entirely, boot from it, partition the disk you chose, create the account and confirm. There is deliberately no script in the repository that picks the drive for you. If your firmware refuses the medium, turn Secure Boot off. Use a spare machine or a separate SSD.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do yet
&lt;/h2&gt;

&lt;p&gt;No physical hardware has been tested at all. The one proven installation is virtual, legacy BIOS, one ext4 partition, no encryption and no swap, and nobody has logged into the installed system. That installed system also carries the live image's initramfs, because nothing in the install sequence regenerates one.&lt;/p&gt;

&lt;p&gt;No browser has been launched inside a booted image. The network stack is proven in CI: NetworkManager active, an IPv4 address, a real name resolved over DNS.&lt;/p&gt;

&lt;p&gt;The ISO is about 2.4 GB, which is 2,432,696,320 bytes for the last measured build. The next one is expected to be about 3.5 GB with the assistant bundle, and that figure is arithmetic, not a measurement.&lt;/p&gt;

&lt;p&gt;The live session has no persistence, the desktop is X11 only, the theme is colour-only so window decorations use the upstream xfwm4 theme, and an upgrade of Debian's &lt;code&gt;base-files&lt;/code&gt; can overwrite &lt;code&gt;/etc/os-release&lt;/code&gt; and make the system identify as Debian. &lt;code&gt;/etc/astraos-release&lt;/code&gt; is the file the project owns, and it is safe.&lt;/p&gt;

&lt;p&gt;Wi-Fi is untested, no proprietary GPU driver is shipped, and the firmware for older Broadcom cards cannot be in the image because Debian does not redistribute it.&lt;/p&gt;

&lt;p&gt;The assistant has no automatic undo wired to an action, no TLS on the paired phone connection, a tamper-evident ledger rather than a signed one, unsandboxed actions, no wake word, no image input, no stored conversation history, and no published answer-quality numbers for real hardware. &lt;code&gt;astra-ai eval&lt;/code&gt; is the instrument that would produce them, and it has only been run as a smoke test on the smallest model in the catalogue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I need
&lt;/h2&gt;

&lt;p&gt;The repository is at &lt;a href="https://github.com/rafikulislammollaq-dev/astraos" rel="noopener noreferrer"&gt;github.com/rafikulislammollaq-dev/astraos&lt;/a&gt;, under the GPL, version 3 or later.&lt;/p&gt;

&lt;p&gt;Three specific things would help more than anything else:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Someone with a spare machine.&lt;/strong&gt; Install it on physical hardware and report what happens. That is the single largest untested area, and a report saying it failed is worth more than a success.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Someone who knows Calamares well enough to review the installer configuration.&lt;/strong&gt; It was written by reading the documentation, not by using the installer every day, and it is the part of the image most likely to be wrong in a way nobody has noticed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wi-Fi chipset results.&lt;/strong&gt; Which chipsets work, which need firmware that is not in the image, and which need drivers this project will not ship.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Translations are wanted too: the installer and the AstraOS utilities are English only. And if a claim in the README is wrong, say so. Every claim in it is tied to a CI run, a test or an artifact, and "untested" is a word I use on purpose.&lt;/p&gt;

</description>
      <category>linux</category>
    </item>
    <item>
      <title>Vibe coding security: the step nobody takes after the build</title>
      <dc:creator>Sukanya Mondol</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:51:50 +0000</pubDate>
      <link>https://dev.to/sukanyamondol/vibe-coding-security-the-step-nobody-takes-after-the-build-cm8</link>
      <guid>https://dev.to/sukanyamondol/vibe-coding-security-the-step-nobody-takes-after-the-build-cm8</guid>
      <description>&lt;p&gt;Here is the loop most apps are built in now. You describe what you want. The model writes it. It runs. You ship it. The loop is fast and it works, and that is why it is everywhere.&lt;/p&gt;

&lt;p&gt;There is a step missing from it. Nothing in the loop looks at the code for security. Not the model, because it wrote the code and cannot see what it missed. Not the platform: Lovable's own blog says its Security Scan "catches many issues, but sometimes incorrect database security setups slip through" (Lovable's blog). And not you, because you are not reading the code, and you were never going to.&lt;/p&gt;

&lt;p&gt;The 2026 measurements of AI-built apps are consistent enough to take seriously. A June 2026 scan of 62 Lovable apps found 63% carried critical or high severity vulnerabilities, with 10 findings per app on average (DEV.to, Jun 2026). VibeSafe studied 400 public repositories built with Lovable, Bolt and v0: 59% had at least one critical issue, and 18.5% had committed a &lt;code&gt;.env&lt;/code&gt; file. Escape DAST analysed 5,600 vibe-coded apps and found 2,038 critical vulnerabilities and 400+ exposed credentials (via Lifehacker, Aug 2026). symbioticsec.ai found that 98% of apps behind a public Supabase URL had at least one exploitable flaw. And vibe-eval measured 37.6% more critical vulnerabilities after just five AI build iterations, so keeping the prompts coming makes the gap wider.&lt;/p&gt;

&lt;h4&gt;
  
  
  The same five mistakes, every time
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;The AI writes the happy path and never checks the answer.&lt;/strong&gt;&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;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;getCurrentUser&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;}&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&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;&lt;code&gt;getUser()&lt;/code&gt; returns &lt;code&gt;{ data, error }&lt;/code&gt;. If nobody is signed in, &lt;code&gt;user&lt;/code&gt; is null and &lt;code&gt;user.id&lt;/code&gt; throws. If the request fails, the code carries on with an undefined user. The check is three lines:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&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="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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The server reads a session it never verified.&lt;/strong&gt;&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;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;readSession&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;}&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSession&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;session&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="nx"&gt;id&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;&lt;code&gt;getSession()&lt;/code&gt; reads the session cookie but does not check the signature with the authentication server. On the client, that is fine, because the cookie only decides what to render. On the server, it means the visitor answers the question "who is this". A hand-edited cookie is accepted as valid. On the server, ask the auth server instead:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&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="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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A permission check that trusts the browser.&lt;/strong&gt;&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isAdmin&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;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browser storage is entirely under the control of the person using the browser. Change the value in the developer tools and the admin panel opens. Permissions belong on the server, and in the database rules, not in a value the visitor can edit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A token check that accepts an unsigned token.&lt;/strong&gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt; &lt;span class="o"&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;JWT_SECRET&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change-me-in-production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&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="s1"&gt;HS256&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two problems. &lt;code&gt;'none'&lt;/code&gt; means a token with no signature at all is accepted, and anyone can write one by hand claiming to be any user, including an admin. The fallback means that if the environment variable is ever missing in production, every token gets signed with a value written in plain sight in the source. The fix is to fail loudly and pin the algorithm:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&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;JWT_SECRET&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;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JWT_SECRET is not set&lt;/span&gt;&lt;span class="dl"&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HS256&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A table with no rule on it.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bookings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;shipping_address&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&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;Nothing after that statement turns on Row Level Security. The full fix, including the policies for each operation, is in the RLS guide linked below.&lt;/p&gt;

&lt;h4&gt;
  
  
  What this means for you
&lt;/h4&gt;

&lt;p&gt;None of this says the code is bad. AI-generated code is often good code. The gap is a missing verification step, and the fix is to add it once, not to rebuild anything. The mistakes above are all small edits to small places.&lt;/p&gt;

&lt;p&gt;It is worth being precise about what the numbers do and do not say. They describe a population of apps that were scanned, and the way those apps were built is not random. They are apps where a person asked a model to produce a working product quickly, and then shipped it. That population describes most AI-built apps, which is why the same handful of mistakes keeps showing up in it. It is not a claim that every AI-built app is broken, and it is not a claim that any particular app of yours is broken. It is a claim that nobody has looked yet.&lt;/p&gt;

&lt;p&gt;What changes when somebody does look is that the list of unknowns gets shorter. The first pass usually finds a handful of specific things, and most of them are one or two lines to change. The second pass finds less. The third finds almost nothing, and you can stop.&lt;/p&gt;

&lt;h4&gt;
  
  
  What to do next
&lt;/h4&gt;

&lt;p&gt;Run the free scanner. It reads your project folder in your browser and checks the six things that matter most in an AI-built project: environment files committed to git, hard-coded secrets, row-level security on your tables, token verification, cross-origin rules, and leftover debug routes. Nothing is uploaded, there is no account, and there is no email address. You get a ship-readiness score out of 100 and a paste-ready fix for every finding. It is not proof that your app is secure. It is a record of what was checked and what was not, which is the step the build loop skipped.&lt;/p&gt;




&lt;p&gt;The scanner that finds this is open source: &lt;a href="https://github.com/rafikulislammollaq-dev/vibeaudit" rel="noopener noreferrer"&gt;github.com/rafikulislammollaq-dev/vibeaudit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also runs in the browser, where your project is read on your own machine and never uploaded: &lt;a href="https://rafikulislammollaq-dev.github.io/vibeaudit/scanner/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit/scanner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you would rather have a person read the findings and explain them, that is a $199 review: &lt;a href="https://rafikulislammulq-dev.github.io/vibeaudit/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>How to check if your app leaks data</title>
      <dc:creator>Sukanya Mondol</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:51:01 +0000</pubDate>
      <link>https://dev.to/sukanyamondol/how-to-check-if-your-app-leaks-data-2oo6</link>
      <guid>https://dev.to/sukanyamondol/how-to-check-if-your-app-leaks-data-2oo6</guid>
      <description>&lt;p&gt;You want to know whether strangers can read your app's data. You can find out yourself tonight, in about ten minutes, without paying anyone. Here is the method, in the order to run it.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Find the public key and the project URL
&lt;/h4&gt;

&lt;p&gt;They are in your frontend bundle on purpose, so this is easy. Open the developer tools, the Network tab, reload the page, and look for the request that goes to &lt;code&gt;*.supabase.co&lt;/code&gt;. Or grep the build output:&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;-ro&lt;/span&gt; &lt;span class="s2"&gt;"https://[a-z0-9]*&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;supabase&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;co"&lt;/span&gt; dist/ | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key next to it is the "anon" key, sometimes labelled &lt;code&gt;VITE_SUPABASE_ANON_KEY&lt;/code&gt; or &lt;code&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/code&gt;. It is public by design. It is not the thing that protects your data, and the rule that does protect your data lives in Postgres, not in the key. If you cannot find either one, look in the environment files of the project, or in the settings of the tool you built the app with.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Send one read request
&lt;/h4&gt;

&lt;p&gt;This is the single most informative test you can run. Use a table name that really exists in your project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*&amp;amp;limit=1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: YOUR_ANON_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_ANON_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three possible answers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;200 with a row of data.&lt;/strong&gt; Anybody on the internet can read this table. This is the one that matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;200 with an empty array.&lt;/strong&gt; The read was allowed, but the table has no rows. Nothing has leaked yet. The door is open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;401 or 403.&lt;/strong&gt; This table is protected. That rule is working.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Send it once, then stop. Do not loop it across tables or pages. That is how a check turns into an incident, and also how you end up holding a copy of your customers' data. The live check in the scanner behaves the same way on purpose: one read request, one table name that already appeared in your own SQL files, no sign-in attempt, nothing written.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Check your migrations for the rule that should have stopped it
&lt;/h4&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;"enable row level security"&lt;/span&gt; supabase/migrations/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"create policy"&lt;/span&gt; supabase/migrations/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every table that holds user data should appear in the first list. Every table in the first list should have at least one policy in the second list. If a table has RLS on and no policy, it has locked itself, including your own signed-in users, and it usually shows up as "the app stopped working". If a table has neither, it is the case from step two. The full fix, with the policy for each operation, is in the RLS guide linked below.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Look for the other four leaks
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;The committed environment file.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git ls-files | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^\.env'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it prints a file, the secrets in it are in your git history and in every clone of the repository. 18.5% of 400 public repositories built with Lovable, Bolt and v0 had a committed &lt;code&gt;.env&lt;/code&gt; (VibeSafe). Rotating the keys is the part that actually protects you. Removing the file does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The admin key in the browser bundle.&lt;/strong&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"service_role"&lt;/span&gt; dist/ build/ .next/ out/ 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service_role key bypasses every row-level rule. If it is in the JavaScript that every visitor downloads, then every visitor has it. Moving it server-side is the fix, and so is rotating it afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The debug route.&lt;/strong&gt;&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="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/debug&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&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;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;env&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;express.static('.')&lt;/code&gt; serves the whole project folder, which means a request for &lt;code&gt;/.env&lt;/code&gt; returns your secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The wildcard cross-origin rule.&lt;/strong&gt;&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="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="s1"&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="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&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="s1"&gt;Access-Control-Allow-Credentials&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="s1"&gt;true&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;That combination tells every browser on the internet that any website may call your API using the visitor's session, and read the response.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Write down what you found and what you did not check
&lt;/h4&gt;

&lt;p&gt;This is the part most people skip, and it is the part that matters. A check you did not run is not a pass. "I looked at two tables out of nine" is a fact about your coverage, and it is worth writing down next to the results.&lt;/p&gt;

&lt;p&gt;If you do this for one app, the habit is worth more than the answer, because the same five checks catch the same classes of problem in the next project too.&lt;/p&gt;

&lt;p&gt;If you would rather have a person read the results than a list, that is a different product, and it is worth comparing the two before you decide.&lt;/p&gt;

&lt;h4&gt;
  
  
  What to do next
&lt;/h4&gt;

&lt;p&gt;Run the free scanner. It reads your project folder in your browser and covers steps 3 and 4 in one pass, plus a check on token verification. Nothing is uploaded, there is no server, no account and no email address. You get a ship-readiness score out of 100, a paste-ready fix for every finding, and a list of the checks that passed and the ones that were skipped and why. It does not make the live request from step two, because that needs a live URL and your explicit permission, so run that one yourself. It is not proof that your app is secure. It is a record of one pass over your files, and it is worth running again after every build.&lt;/p&gt;




&lt;p&gt;The scanner that finds this is open source: &lt;a href="https://github.com/rafikulislammollaq-dev/vibeaudit" rel="noopener noreferrer"&gt;github.com/rafikulislammollaq-dev/vibeaudit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also runs in the browser, where your project is read on your own machine and never uploaded: &lt;a href="https://rafikulislammollaq-dev.github.io/vibeaudit/scanner/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit/scanner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you would rather have a person read the findings and explain them, that is a $199 review: &lt;a href="https://rafikulislammollaq-dev.github.io/vibeaudit/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Vibe coding security: the step nobody takes after the build</title>
      <dc:creator>Sukanya Mondol</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:46:17 +0000</pubDate>
      <link>https://dev.to/sukanyamondol/vibe-coding-security-the-step-nobody-takes-after-the-build-3amk</link>
      <guid>https://dev.to/sukanyamondol/vibe-coding-security-the-step-nobody-takes-after-the-build-3amk</guid>
      <description>&lt;p&gt;Here is the loop most apps are built in now. You describe what you want. The model writes it. It runs. You ship it. The loop is fast and it works, and that is why it is everywhere.&lt;/p&gt;

&lt;p&gt;There is a step missing from it. Nothing in the loop looks at the code for security. Not the model, because it wrote the code and cannot see what it missed. Not the platform: Lovable's own blog says its Security Scan "catches many issues, but sometimes incorrect database security setups slip through" (Lovable's blog). And not you, because you are not reading the code, and you were never going to.&lt;/p&gt;

&lt;p&gt;The 2026 measurements of AI-built apps are consistent enough to take seriously. A June 2026 scan of 62 Lovable apps found 63% carried critical or high severity vulnerabilities, with 10 findings per app on average (DEV.to, Jun 2026). VibeSafe studied 400 public repositories built with Lovable, Bolt and v0: 59% had at least one critical issue, and 18.5% had committed a &lt;code&gt;.env&lt;/code&gt; file. Escape DAST analysed 5,600 vibe-coded apps and found 2,038 critical vulnerabilities and 400+ exposed credentials (via Lifehacker, Aug 2026). symbioticsec.ai found that 98% of apps behind a public Supabase URL had at least one exploitable flaw. And vibe-eval measured 37.6% more critical vulnerabilities after just five AI build iterations, so keeping the prompts coming makes the gap wider.&lt;/p&gt;

&lt;h4&gt;
  
  
  The same five mistakes, every time
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;The AI writes the happy path and never checks the answer.&lt;/strong&gt;&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;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;getCurrentUser&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;}&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&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;&lt;code&gt;getUser()&lt;/code&gt; returns &lt;code&gt;{ data, error }&lt;/code&gt;. If nobody is signed in, &lt;code&gt;user&lt;/code&gt; is null and &lt;code&gt;user.id&lt;/code&gt; throws. If the request fails, the code carries on with an undefined user. The check is three lines:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&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="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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The server reads a session it never verified.&lt;/strong&gt;&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;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;readSession&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;}&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSession&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;session&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="nx"&gt;id&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;&lt;code&gt;getSession()&lt;/code&gt; reads the session cookie but does not check the signature with the authentication server. On the client, that is fine, because the cookie only decides what to render. On the server, it means the visitor answers the question "who is this". A hand-edited cookie is accepted as valid. On the server, ask the auth server instead:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&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="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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A permission check that trusts the browser.&lt;/strong&gt;&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isAdmin&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;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browser storage is entirely under the control of the person using the browser. Change the value in the developer tools and the admin panel opens. Permissions belong on the server, and in the database rules, not in a value the visitor can edit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A token check that accepts an unsigned token.&lt;/strong&gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt; &lt;span class="o"&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;JWT_SECRET&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change-me-in-production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&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="s1"&gt;HS256&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two problems. &lt;code&gt;'none'&lt;/code&gt; means a token with no signature at all is accepted, and anyone can write one by hand claiming to be any user, including an admin. The fallback means that if the environment variable is ever missing in production, every token gets signed with a value written in plain sight in the source. The fix is to fail loudly and pin the algorithm:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&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;JWT_SECRET&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;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JWT_SECRET is not set&lt;/span&gt;&lt;span class="dl"&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HS256&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A table with no rule on it.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bookings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;shipping_address&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&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;Nothing after that statement turns on Row Level Security. The full fix, including the policies for each operation, is in the RLS guide linked below.&lt;/p&gt;

&lt;h4&gt;
  
  
  What this means for you
&lt;/h4&gt;

&lt;p&gt;None of this says the code is bad. AI-generated code is often good code. The gap is a missing verification step, and the fix is to add it once, not to rebuild anything. The mistakes above are all small edits to small places.&lt;/p&gt;

&lt;p&gt;It is worth being precise about what the numbers do and do not say. They describe a population of apps that were scanned, and the way those apps were built is not random. They are apps where a person asked a model to produce a working product quickly, and then shipped it. That population describes most AI-built apps, which is why the same handful of mistakes keeps showing up in it. It is not a claim that every AI-built app is broken, and it is not a claim that any particular app of yours is broken. It is a claim that nobody has looked yet.&lt;/p&gt;

&lt;p&gt;What changes when somebody does look is that the list of unknowns gets shorter. The first pass usually finds a handful of specific things, and most of them are one or two lines to change. The second pass finds less. The third finds almost nothing, and you can stop.&lt;/p&gt;

&lt;h4&gt;
  
  
  What to do next
&lt;/h4&gt;

&lt;p&gt;Run the free scanner. It reads your project folder in your browser and checks the six things that matter most in an AI-built project: environment files committed to git, hard-coded secrets, row-level security on your tables, token verification, cross-origin rules, and leftover debug routes. Nothing is uploaded, there is no account, and there is no email address. You get a ship-readiness score out of 100 and a paste-ready fix for every finding. It is not proof that your app is secure. It is a record of what was checked and what was not, which is the step the build loop skipped.&lt;/p&gt;




&lt;p&gt;The scanner that finds this is open source: &lt;a href="https://github.com/rafikulislammollaq-dev/vibeaudit" rel="noopener noreferrer"&gt;github.com/rafikulislammollaq-dev/vibeaudit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also runs in the browser, where your project is read on your own machine and never uploaded: &lt;a href="https://rafikulislammollaq-dev.github.io/vibeaudit/scanner/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit/scanner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you would rather have a person read the findings and explain them, that is a $199 review: &lt;a href="https://rafikulislammollaq-dev.github.io/vibeaudit/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>Is Lovable safe to ship? The checks that decide it</title>
      <dc:creator>Sukanya Mondol</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:40:59 +0000</pubDate>
      <link>https://dev.to/sukanyamondol/is-lovable-safe-to-ship-the-checks-that-decide-it-59oa</link>
      <guid>https://dev.to/sukanyamondol/is-lovable-safe-to-ship-the-checks-that-decide-it-59oa</guid>
      <description>&lt;p&gt;You built it in a weekend. It looks good, the demo works, and now you have to decide whether to point real users and real data at it. That is a reasonable thing to be nervous about, because the platform cannot answer the question for you. Lovable says so itself. Its own blog puts it plainly: "The Security Scan catches many issues, but sometimes incorrect database security setups slip through… accidentally hardcoding API secrets… or leaving Supabase Edge Function endpoints unprotected." (Lovable's blog)&lt;/p&gt;

&lt;p&gt;The measurements behind that gap are not theoretical. A June 2026 scan of 62 Lovable apps found 63% carried critical or high severity vulnerabilities, with an average of 10 findings per app (DEV.to, Jun 2026). VibeSafe studied 400 public repositories built with Lovable, Bolt and v0 and found 59% had at least one critical issue, while 18.5% had committed a &lt;code&gt;.env&lt;/code&gt; file to the repository. Escape DAST analysed 5,600 vibe-coded apps and found 2,038 critical vulnerabilities and 400+ exposed credentials (via Lifehacker, Aug 2026). CVE-2025-48757, the 2025 Lovable breach, exposed 170 production apps (cited in the DEV.to checklist). That last one is context, not a threat to you. It shows what a shared class of infrastructure can do.&lt;/p&gt;

&lt;p&gt;So the useful question is not whether Lovable is safe. It is which specific things to look at before you ship.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The Edge Function that trusts the browser
&lt;/h4&gt;

&lt;p&gt;This is the classic Lovable shape: an Edge Function holding the admin key, reading the session out of the cookie, and taking the target account from the request body.&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;// supabase/functions/delete-user/index.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabaseAdmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;Deno&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Deno&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;}&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;supabaseAdmin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSession&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;userId&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="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;target_user_id&lt;/span&gt; &lt;span class="p"&gt;}&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;req&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&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;supabaseAdmin&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profiles&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="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target_user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three problems in a dozen lines. Edge Functions run with the service_role key, which bypasses Row Level Security completely, so nothing downstream will stop that delete. &lt;code&gt;getSession()&lt;/code&gt; reads a cookie without re-checking its signature with the auth server, so the "who is calling" answer comes from the caller. And &lt;code&gt;target_user_id&lt;/code&gt; comes from the request body, so the caller chooses whose account gets deleted.&lt;/p&gt;

&lt;p&gt;The fix is to verify the caller on the server and never take an identity from the request:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&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;supabaseAdmin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&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="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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;target_user_id&lt;/span&gt; &lt;span class="p"&gt;}&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;req&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check that the verified caller is allowed to delete that account, usually an admin role or their own account, before running the delete. The scanner's authorisation check flags the unverified session and the request body separately, and the row-level security check deals with the third.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. The environment file that got committed
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git ls-files | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^\.env'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that prints a file, your database admin key and your live payment keys are in your git history, on your hosting provider and in every clone of the repository. Deleting the file does not remove it. The key has to be rotated in the provider's dashboard, and &lt;code&gt;.env&lt;/code&gt; has to go into &lt;code&gt;.gitignore&lt;/code&gt;. This was true in 18.5% of the 400 public repositories VibeSafe looked at.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. The debug route
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/debug&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&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;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;env&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="na"&gt;serviceRoleKey&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;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stripeKey&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;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One route, three secrets, handed to anyone who types the address. AI assistants add these while building and usually forget them. Deleting the route is the fix. If you still need it, block it in production.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. The admin page with no check
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&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;supabaseAdmin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profiles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&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;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&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 path says admin. Nothing in the handler verifies that the caller is one. The scanner looks for admin-shaped routes and checks the handler for a role test.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. The admin key in the browser bundle
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/supabase.js - this file is compiled into the JavaScript&lt;/span&gt;
&lt;span class="c1"&gt;// that every visitor downloads&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&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;VITE_SUPABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&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;VITE_SUPABASE_SERVICE_ROLE_KEY&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 &lt;code&gt;service_role&lt;/code&gt; key bypasses every row-level rule. With a &lt;code&gt;VITE_&lt;/code&gt; prefix, the bundler inlines it into the JavaScript, so every visitor ends up holding a full database admin key. Escape DAST found 400+ exposed credentials across the 5,600 vibe-coded apps it analysed (via Lifehacker, Aug 2026). The fix is to rename the variable so it no longer carries a browser-exposed prefix, move every call that uses it to an Edge Function or a server route, rebuild, delete the old build folder, and rotate the key afterwards.&lt;/p&gt;

&lt;p&gt;One more fact worth knowing before you keep building. Shipping more features makes this worse, not better: vibe-eval measured 37.6% more critical vulnerabilities after just five AI build iterations.&lt;/p&gt;

&lt;h4&gt;
  
  
  What to do next
&lt;/h4&gt;

&lt;p&gt;These five checks are exactly what the $199 review covers, run across your code, migrations, git history and build output in 48 hours at a fixed price. You get the findings ranked by how much they would hurt, the fix for each one written out, and a recorded walkthrough so you can follow it. It is a code and configuration review, not a penetration test, and it is not a guarantee that your app is secure. It is a list of what was checked, what was found, and what is still unknown.&lt;/p&gt;




&lt;p&gt;The scanner that finds this is open source: &lt;a href="https://github.com/rafikulislammollaq-dev/vibeaudit" rel="noopener noreferrer"&gt;github.com/rafikulislammollaq-dev/vibeaudit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also runs in the browser, where your project is read on your own machine and never uploaded: &lt;a href="https://rafikulislammollaq-dev.github.io/vibeaudit/scanner/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit/scanner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you would rather have a person read the findings and explain them, that is a $199 review: &lt;a href="https://rafikulislammollaq-dev.github.io/vibeaudit/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Supabase RLS not enabled: what it means and how to fix it</title>
      <dc:creator>Sukanya Mondol</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:40:24 +0000</pubDate>
      <link>https://dev.to/sukanyamondol/supabase-rls-not-enabled-what-it-means-and-how-to-fix-it-2lgo</link>
      <guid>https://dev.to/sukanyamondol/supabase-rls-not-enabled-what-it-means-and-how-to-fix-it-2lgo</guid>
      <description>&lt;p&gt;Your app is live. The URL works, the database has real rows in it, and somebody has just opened the browser developer tools, found the Supabase project URL and the public "anon" key that your frontend is built to expose, and sent one request to your database. If that request comes back with your customers' names, emails and addresses, your tables have no Row Level Security. The key was never the problem.&lt;/p&gt;

&lt;p&gt;This is not a rare failure. In the 2026 vibe-eval benchmark, 59% of the apps sampled had at least one RLS gap, and 10.3% of sampled Lovable apps exposed real user data through missing Row Level Security. symbioticsec.ai reported that 98% of apps behind a public Supabase URL had at least one exploitable flaw.&lt;/p&gt;

&lt;p&gt;The design explains why this happens. The project URL and the anon key are public on purpose. They ship inside your JavaScript bundle, and Supabase grants the anon role access to your tables by default. Your only defence is a rule inside Postgres that says which signed-in user may read which row. If that rule was never written, the public key reads everything.&lt;/p&gt;

&lt;h4&gt;
  
  
  What "RLS not enabled" looks like in your code
&lt;/h4&gt;

&lt;p&gt;The scanner reads your SQL migrations. This is the pattern it finds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;cascade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;full_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;company&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&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;A table holding names, emails and phone numbers, and no &lt;code&gt;enable row level security&lt;/code&gt; anywhere in the file. Postgres does not filter rows for that table at all, so the anon role gets whatever the default grants give it.&lt;/p&gt;

&lt;p&gt;Three more shapes count as the same problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RLS is on, but the policy underneath allows everything.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"anyone can read orders"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&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;The table looks protected in the dashboard. The rule allows every row for every role, so the effect is identical to having no rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access granted straight to the public role.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That hands the unauthenticated role full access to a table with no RLS on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A function that runs with admin rights and no pinned search path.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_company_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
&lt;span class="k"&gt;security&lt;/span&gt; &lt;span class="k"&gt;definer&lt;/span&gt;
&lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;declare&lt;/span&gt;
  &lt;span class="k"&gt;result&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'employees'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'employee'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'plan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;companies&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;company_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="k"&gt;result&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;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;security definer&lt;/code&gt; means the function runs with the privileges of its owner, usually the database owner. Without &lt;code&gt;set search_path&lt;/code&gt;, a caller can hijack the path and make the function resolve objects they control. This is the classic Postgres privilege-escalation path.&lt;/p&gt;

&lt;h4&gt;
  
  
  The fix
&lt;/h4&gt;

&lt;p&gt;Turn the rule on, then write it per table. A table that belongs to a person:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users read own row"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users update own row"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A table that belongs to a company:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"owners read own company"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;companies&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin the search path on every elevated function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_company_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
&lt;span class="k"&gt;security&lt;/span&gt; &lt;span class="k"&gt;definer&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pg_temp&lt;/span&gt;
&lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One trap sits on the other side. If you enable RLS and write no policy at all, the table locks itself. Nobody can read it, including your signed-in users, and it usually shows up as "the app stopped working" after a fix. Write a policy for every table you enable, and add &lt;code&gt;insert&lt;/code&gt; and &lt;code&gt;delete&lt;/code&gt; if the app needs them.&lt;/p&gt;

&lt;h4&gt;
  
  
  The table that is not in your code
&lt;/h4&gt;

&lt;p&gt;One more case is worth knowing about. Tables created straight in the Supabase dashboard leave no migration in your repository, so nothing in the code records whether row-level security was turned on for them. The scanner reports those as tables your app uses that it could not review, rather than claiming they are safe. The fix is to export the schema from the Supabase SQL editor and commit it to the repository, so the access rules are visible and reviewable. Dashboards are exactly where row-level security gets forgotten, because there is no pull request where somebody would notice.&lt;/p&gt;

&lt;h4&gt;
  
  
  Checking it in one request
&lt;/h4&gt;

&lt;p&gt;Once the policies are in, the same anonymous request that used to return your customers should come back empty, or 401, or 403. That is the live check in the scanner. It sends exactly one read request for one table, and it never signs in. It also reports what it could not check, so a table it never saw is never counted as a pass.&lt;/p&gt;

&lt;h4&gt;
  
  
  What to do next
&lt;/h4&gt;

&lt;p&gt;A scanner will name the table. It will not write the policy that fits your schema, and it will not tell you which of the four operations your app actually needs. The $199 review does that. It reads your migrations, lists every table that needs a policy, and hands you the SQL for each one so you can paste it straight into your builder. 48 hours, fixed price. It is a review of your code, not a penetration test of your live site, and it is not a guarantee that your app is secure.&lt;/p&gt;




&lt;p&gt;The scanner that finds this is open source: &lt;a href="https://github.com/rafikulislammollaq-dev/vibeaudit" rel="noopener noreferrer"&gt;github.com/rafikulislammollaq-dev/vibeaudit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also runs in the browser, where your project is read on your own machine and never uploaded: &lt;a href="https://rafikulislammollaq-dev.github.io/vibeaudit/scanner/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit/scanner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you would rather have a person read the findings and explain them, that is a $199 review: &lt;a href="https://rafikulislammollaq-dev.github.io/vibeaudit/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>database</category>
      <category>postgres</category>
      <category>security</category>
    </item>
    <item>
      <title>vibeaudit: 11 static checks for the security holes AI-built apps ship with</title>
      <dc:creator>Sukanya Mondol</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:19:35 +0000</pubDate>
      <link>https://dev.to/sukanyamondol/vibeaudit-11-static-checks-for-the-security-holes-ai-built-apps-ship-with-3f7g</link>
      <guid>https://dev.to/sukanyamondol/vibeaudit-11-static-checks-for-the-security-holes-ai-built-apps-ship-with-3f7g</guid>
      <description>&lt;p&gt;The last few years of app building went like this: you describe what you want, a model writes the code, the code runs, you ship it. The tooling got dramatically faster; the verification step did not happen at all, and nobody - including the model that wrote the code - was checking.&lt;/p&gt;

&lt;p&gt;The numbers are not theoretical. A June 2026 study on DEV.to scanned 62 Lovable apps: &lt;strong&gt;63% had critical or high severity vulnerabilities&lt;/strong&gt;, 10 findings per app on average. A separate study of 400 public Lovable, Bolt and v0 repositories, published by VibeSafe, found &lt;strong&gt;59% had at least one critical issue&lt;/strong&gt; and &lt;strong&gt;18.5% had committed a &lt;code&gt;.env&lt;/code&gt; file&lt;/strong&gt; to git.&lt;/p&gt;

&lt;p&gt;These are not random bugs. The same handful of mistakes keep showing up, in the same order of severity. That makes them automatable, and it is why I built &lt;code&gt;vibeaudit&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What vibeaudit actually does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;vibeaudit&lt;/code&gt; is a command line tool. Point it at a project folder and it reads the code, the SQL migrations, the git history and the build output, then produces a ship-readiness report.&lt;/p&gt;

&lt;p&gt;Three properties mattered more to me than the detection itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero runtime dependencies.&lt;/strong&gt; Node 20+ and the standard library only. No &lt;code&gt;npm install&lt;/code&gt;, no build step, no package tree to trust on a client's machine. The only outbound call is &lt;code&gt;npm audit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every finding carries a fix, not just a label.&lt;/strong&gt; File, line, CWE reference, the offending code with the secret redacted, the exact replacement, and a paste-ready prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It reports what it did not find.&lt;/strong&gt; Passed checks and skipped checks are both listed, with the reason. "Skipped" is never reported as "passed".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 11 checks, grouped by what they protect
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What it finds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;&lt;code&gt;env-committed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.env&lt;/code&gt; committed to git, or present and not &lt;code&gt;.gitignore&lt;/code&gt;d&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;&lt;code&gt;secrets-source&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;API keys, Stripe keys, &lt;code&gt;service_role&lt;/code&gt; JWTs, AWS keys, private keys, webhooks - in source &lt;strong&gt;and git history&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;&lt;code&gt;secrets-bundle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Admin keys, live Stripe keys and private keys inside the built client bundle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB access control&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tables without &lt;code&gt;ENABLE ROW LEVEL SECURITY&lt;/code&gt;; unsafe &lt;code&gt;SECURITY DEFINER&lt;/code&gt;; permissive &lt;code&gt;USING (true)&lt;/code&gt;; &lt;code&gt;GRANT&lt;/code&gt; to &lt;code&gt;anon&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB access control&lt;/td&gt;
&lt;td&gt;&lt;code&gt;live-supabase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Admin key in browser code, plus one authorised live read that proves whether a table is readable without signing in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;&lt;code&gt;authz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unchecked &lt;code&gt;getUser()&lt;/code&gt;; &lt;code&gt;getSession()&lt;/code&gt; on the server; &lt;code&gt;user_id&lt;/code&gt;/&lt;code&gt;role&lt;/code&gt; trusted from the body or &lt;code&gt;localStorage&lt;/code&gt;; admin routes with no role check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jwt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;alg: "none"&lt;/code&gt; accepted; &lt;code&gt;jwt.decode()&lt;/code&gt; where &lt;code&gt;verify()&lt;/code&gt; was needed; signing secret falling back to a hard-coded value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wildcard origin with credentials; a reflected &lt;code&gt;Origin&lt;/code&gt;; &lt;code&gt;cors({ origin: '*' })&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;&lt;code&gt;live-headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Missing HSTS, CSP, &lt;code&gt;X-Content-Type-Options&lt;/code&gt;, &lt;code&gt;Referrer-Policy&lt;/code&gt;, &lt;code&gt;X-Frame-Options&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supply chain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;debug-routes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/api/debug&lt;/code&gt; and friends; &lt;code&gt;express.static('.')&lt;/code&gt; (publishes &lt;code&gt;/.env&lt;/code&gt;); source maps in production; GraphQL introspection left on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supply chain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm audit&lt;/code&gt; severities, plus dependency names that are bad defaults for AI-built apps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three checks cover secrets because a key can leak three ways: into a committed file, into source, into the JavaScript every visitor downloads. Deleting the file is not enough, which is why &lt;code&gt;secrets-source&lt;/code&gt; also reads the git history - a secret that was committed and later removed is still recoverable, and still valid. Detection is 16 credential patterns, plus JWT handling that decodes the payload to tell a harmless &lt;code&gt;anon&lt;/code&gt; key from a deadly &lt;code&gt;service_role&lt;/code&gt; key. The database pair is the highest-value part of the tool and the part a linter does not do: with Row Level Security off, the Supabase REST endpoint returns every row to anyone holding the public anon key - which ships inside your app by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoring model
&lt;/h2&gt;

&lt;p&gt;The score starts at 100 and every finding deducts points:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CRITICAL&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;Customer data or money is exposed, or an attacker can impersonate somebody&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HIGH&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;A serious hole a targeted attacker would find quickly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MEDIUM&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Worth fixing before launch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LOW&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Hygiene&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;score = max(0, 100 - 25*critical - 12*high - 4*medium - 1*low)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The weights are chosen so that four criticals, or one critical plus a few highs, can sink a project on their own. A straight subtraction is trivially explainable to a client ("each critical finding costs 25 points") and deterministic, so two audits produce comparable numbers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;90-100&lt;/td&gt;
&lt;td&gt;Ready to ship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70-89&lt;/td&gt;
&lt;td&gt;Ship after fixing high findings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40-69&lt;/td&gt;
&lt;td&gt;Do not ship yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0-39&lt;/td&gt;
&lt;td&gt;Unsafe to ship&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;One rule sits on top of that: a single critical finding caps the verdict at "Do not ship yet", whatever the score.&lt;/strong&gt; A score must never be able to hide a problem that puts customer data at risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually prints
&lt;/h2&gt;

&lt;p&gt;Run against the bundled demo project - a small, deliberately broken app - here is the real output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ship readiness: 0/100 - Unsafe to ship

| Critical | High | Medium | Low |
|---|---|---|---|
| 12 | 20 | 7 | 2 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the plain-English paragraph that follows it, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We checked 16 files in your project for the security mistakes that AI-built apps make most often - leaked passwords and keys, database tables that anyone on the internet can read, and admin pages that are not protected. We found 41 issues - 12 serious problems, 20 important problems, 7 medium problems and 2 small problems. That gives a readiness score of 0 out of 100, which means the app currently has serious security holes and should not be exposed to users or real data until they are fixed. The most urgent one is: A powerful secret key is sitting in your project files. Anyone who gets hold of it can act as you, followed by stripe live secret key found in .env. All of these can be fixed without rebuilding the app. We only looked at the files you gave us - we did not test your live site, so the online parts of your app still need a separate check.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The evidence keeps a short prefix so you can tell which key it is; the rest never appears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJI************************[REDACTED len=203] role=service_role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the report's design: the reader is usually a founder who does not read code, so every finding carries a plain-English line, the file and line, the fix, and a prompt to paste back into your assistant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safety design
&lt;/h2&gt;

&lt;p&gt;The live checks touch the network, so they are fenced off.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--url&lt;/code&gt; alone does nothing. The live checks need both &lt;code&gt;--url &amp;lt;url&amp;gt;&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;--i-have-authorization&lt;/code&gt;. Without the flag they are reported as SKIPPED with the reason, never as passed.&lt;/li&gt;
&lt;li&gt;That is not a promise in the README, it is a test. The suite runs a local HTTP server that counts requests: &lt;code&gt;without --i-have-authorization the tool sends NO request at all&lt;/code&gt; and &lt;code&gt;with --i-have-authorization the tool sends exactly ONE read request&lt;/code&gt;, asserting the method is &lt;code&gt;GET&lt;/code&gt; and the path is &lt;code&gt;/&lt;/code&gt;. 69 tests, all passing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never enumerates tables.&lt;/strong&gt; The single Supabase probe uses one table name that already appears in the project's own SQL files and sends exactly one request: &lt;code&gt;GET {project-url}/rest/v1/{table}?select=*&amp;amp;limit=1&lt;/code&gt; with the public anon key. No other table is tried.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never attempts to authenticate.&lt;/strong&gt; No password guessing, no token replay, no directory listing. The only other request is a single &lt;code&gt;GET&lt;/code&gt; to the root of the URL you supplied, to read the response headers.&lt;/li&gt;
&lt;li&gt;It reads at most 64 KB of the response body and gives up after 12 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets are never printed.&lt;/strong&gt; Every secret is replaced with a short identifying prefix and &lt;code&gt;[REDACTED len=...]&lt;/code&gt;. The report gets emailed and pasted into chat windows; a tool that leaks the key it found makes the problem worse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else is offline and static.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two engineering decisions that are not obvious
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The demo fixture's secrets are generated at runtime, not committed.&lt;/strong&gt; GitHub push protection refuses any push containing a string shaped like a live credential - even a fabricated one - and it scans git history as well as the tip. A demo fixture full of realistic keys would make the repository impossible to publish, defeating the purpose of shipping the tool. So &lt;code&gt;fixtures/build-fixture.mjs&lt;/code&gt; writes the vulnerable files at runtime, assembling each credential from parts so no contiguous secret pattern appears in the published source tree, and it creates the fixture's own git repository so the &lt;code&gt;.env&lt;/code&gt; is genuinely committed rather than merely present. The demo still exercises every detection path it is supposed to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Scanning a subdirectory of a repo still finds committed secrets.&lt;/strong&gt; A client sends you one sub-project of a monorepo, and the file that leaks their &lt;code&gt;service_role&lt;/code&gt; key is committed in the parent repository. A naive implementation checks for a &lt;code&gt;.git&lt;/code&gt; directory in the target folder, finds none, and reports "no git history" - losing the finding that matters most. The fix was &lt;code&gt;isGitRepo&lt;/code&gt;, which falls back to &lt;code&gt;git rev-parse --show-toplevel&lt;/code&gt; and walks up to the repository root. &lt;code&gt;git ls-files&lt;/code&gt; and &lt;code&gt;git log&lt;/code&gt; still run from the scanned directory, so the paths they return stay relative to what you asked for and no unrelated project is pulled in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and run
&lt;/h2&gt;

&lt;p&gt;There is nothing to install. Node 20+ is the only requirement.&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. See what it finds, against a bundled, deliberately broken demo app.&lt;/span&gt;
node cli.js &lt;span class="nt"&gt;--demo&lt;/span&gt;

&lt;span class="c"&gt;# 2. Scan a real project.&lt;/span&gt;
node cli.js &lt;span class="nt"&gt;--target&lt;/span&gt; ./some-project

&lt;span class="c"&gt;# 3. Write the report to a file you can send to someone.&lt;/span&gt;
node cli.js &lt;span class="nt"&gt;--target&lt;/span&gt; ./some-project &lt;span class="nt"&gt;--out&lt;/span&gt; report.md

&lt;span class="c"&gt;# 4. Add the live checks. Only if you own the site or have written permission.&lt;/span&gt;
node cli.js &lt;span class="nt"&gt;--target&lt;/span&gt; ./app &lt;span class="nt"&gt;--url&lt;/span&gt; https://app.example.com &lt;span class="nt"&gt;--i-have-authorization&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also useful: &lt;code&gt;--json&lt;/code&gt; for machine-readable output, &lt;code&gt;--no-history&lt;/code&gt; to skip the git history scan, &lt;code&gt;--fail-on-critical&lt;/code&gt; for CI, &lt;code&gt;--quiet&lt;/code&gt; for just the headline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not do
&lt;/h2&gt;

&lt;p&gt;It is not a penetration test. It does not break in, exploit anything, attack your live app, or take a copy of your data, and it certifies nothing. It is not a compliance audit, and it is not a guarantee that your app is secure. It is a focused code and configuration review of the files you gave it - the subset of a real pentest that can be automated. If you need a signed penetration test report for a compliance file, that needs a certified tester, and this is not it. It does not cover your hosting, DNS, firewalls or third-party services beyond the keys it found.&lt;/p&gt;

&lt;p&gt;If you would rather have a person read the results than run the tool yourself, that is the same review sold at a fixed price: $199 for the report, $599 with the fixes implemented, $899 for a pre-launch ship check that adds deployment hardening and a written checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://github.com/rafikulislammollaq-dev/vibeaudit" rel="noopener noreferrer"&gt;https://github.com/rafikulislammollaq-dev/vibeaudit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. Run &lt;code&gt;node --test test/&lt;/code&gt; and you should see 69 passing tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Command line&lt;/strong&gt; (Node 20+, zero dependencies):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/rafikulislammollaq-dev/vibeaudit
&lt;span class="nb"&gt;cd &lt;/span&gt;vibeaudit
node cli.js &lt;span class="nt"&gt;--demo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In your browser, nothing uploaded:&lt;/strong&gt; &lt;a href="https://rafikulislammollaq-dev.github.io/vibeaudit/scanner/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit/scanner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/rafikulislammollaq-dev/vibeaudit" rel="noopener noreferrer"&gt;github.com/rafikulislammollaq-dev/vibeaudit&lt;/a&gt; - MIT licensed.&lt;/p&gt;

&lt;p&gt;If you would rather have a person read the findings and explain them, that is the $199 review: &lt;a href="https://rafikulislammollaq-dev.github.io/vibeaudit/" rel="noopener noreferrer"&gt;rafikulislammollaq-dev.github.io/vibeaudit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>staticanalysis</category>
      <category>security</category>
    </item>
  </channel>
</rss>
