<?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: Alon Trifonov</title>
    <description>The latest articles on DEV Community by Alon Trifonov (@apex97).</description>
    <link>https://dev.to/apex97</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%2F4209559%2Ffb90368b-55ac-474a-9bd3-83f64f9b66d7.png</url>
      <title>DEV Community: Alon Trifonov</title>
      <link>https://dev.to/apex97</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/apex97"/>
    <language>en</language>
    <item>
      <title>5 Security Holes Almost Every AI Generated App Ships With (And How to Fix Them)</title>
      <dc:creator>Alon Trifonov</dc:creator>
      <pubDate>Tue, 27 Oct 2026 11:40:13 +0000</pubDate>
      <link>https://dev.to/apex97/5-security-holes-almost-every-ai-generated-app-ships-with-and-how-to-fix-them-1lj7</link>
      <guid>https://dev.to/apex97/5-security-holes-almost-every-ai-generated-app-ships-with-and-how-to-fix-them-1lj7</guid>
      <description>&lt;p&gt;AI app builders like Lovable, Bolt, and v0 are impressive. You describe what you want, wait a few minutes, and get a working app that looks ready for real users.&lt;/p&gt;

&lt;p&gt;It is not ready for real users.&lt;/p&gt;

&lt;p&gt;I'm Alon Trifonov, founder of Zyntari, a small engineering studio in Israel. A big part of my job is taking AI-generated prototypes and turning them into production software, mostly Python and Flask on the backend. After auditing a bunch of these apps, I keep finding the same five security holes. Not exotic, movie-hacker stuff. The boring classics: the kind that show up in the OWASP Top 10 year after year because developers keep making the same mistakes. Now AI generators are making those same mistakes at scale.&lt;/p&gt;

&lt;p&gt;Every one of them is fixable in under an hour. Here is what to look for, how to check if your app has the problem, and exactly how to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Debug mode left on
&lt;/h2&gt;

&lt;p&gt;Open the main file of your generated app. If you see this, you have a problem:&lt;/p&gt;

&lt;p&gt;if &lt;strong&gt;name&lt;/strong&gt; == "&lt;strong&gt;main&lt;/strong&gt;":&lt;br&gt;
    app.run(debug=True)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's dangerous:&lt;/strong&gt; debug=True enables the Werkzeug debugger, an interactive Python console that runs in your browser. If it ever reaches a live server, anyone who triggers an error in your app gets a console that can run arbitrary code on your machine. The Werkzeug documentation says it in plain words: "the debugger must never be used on production machines."&lt;/p&gt;

&lt;p&gt;This is not a theoretical risk. In 2015, Patreon, a company with millions of users, left the Werkzeug debugger publicly accessible. Security researchers at Detectify warned them about it, and days later attackers walked in through exactly that hole and dumped 15 gigabytes of user data online, including 2.3 million email addresses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check:&lt;/strong&gt; search your whole codebase for "debug". Every match should end up controlled by the environment, never hardcoded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;import os&lt;/p&gt;

&lt;p&gt;if &lt;strong&gt;name&lt;/strong&gt; == "&lt;strong&gt;main&lt;/strong&gt;":&lt;br&gt;
    app.run(debug=os.environ.get("FLASK_DEBUG", "false").lower() == "true")&lt;/p&gt;

&lt;p&gt;And in production, don't use Flask's built-in server at all. Use Gunicorn:&lt;/p&gt;

&lt;p&gt;gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Secrets sitting directly in the code
&lt;/h2&gt;

&lt;p&gt;Generated code loves this pattern:&lt;/p&gt;

&lt;p&gt;app.secret_key = "super-secret-key-123"&lt;br&gt;
STRIPE_API_KEY = "sk_live_..."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's dangerous:&lt;/strong&gt; anything committed to a Git repository should be treated as public. Yes, even in a private repo, because repos get shared, cloned, screenshotted, and pushed by accident. GitGuardian scans every public commit on GitHub, and their State of Secrets Sprawl 2026 report found 28.65 million new hardcoded secrets pushed to public GitHub in 2025 alone, up 34% year over year. Worse for us specifically: AI-assisted commits leaked secrets at roughly double the baseline rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;grep -rInE "(secret|api[_-]?key|password|token)\s*=\s*[\"']" . --include="*.py"&lt;/p&gt;

&lt;p&gt;If that command prints anything that looks like a real value, that is your to-do list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; secrets come from the environment, never from source code:&lt;/p&gt;

&lt;p&gt;import os&lt;/p&gt;

&lt;p&gt;app.secret_key = os.environ.get("SECRET_KEY")&lt;br&gt;
if not app.secret_key:&lt;br&gt;
    raise RuntimeError("SECRET_KEY is not set. Refusing to start insecurely.")&lt;/p&gt;

&lt;p&gt;Generate a proper key:&lt;/p&gt;

&lt;p&gt;python -c "import secrets; print(secrets.token_hex(32))"&lt;/p&gt;

&lt;p&gt;Then set it wherever you deploy. Render, Railway, and Docker all have an environment variables section for exactly this. And if a secret already leaked: rotate it. Deleting the commit does nothing. The value already lives in the repo history and in every scanner's database.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. SQL injection hiding in "dynamic" queries
&lt;/h2&gt;

&lt;p&gt;Generators usually do the right thing and use an ORM like SQLAlchemy. The trouble starts when the AI needs something "dynamic," like a search filter, and reaches for string formatting:&lt;/p&gt;

&lt;p&gt;email = request.args.get("email")&lt;br&gt;
query = f"SELECT * FROM users WHERE email = '{email}'"&lt;br&gt;
result = db.session.execute(query)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's dangerous:&lt;/strong&gt; that f-string is a door directly into your database. A user who types ' OR '1'='1' -- as their email just bypassed your login. Injection has been in the top three of the OWASP Top 10 for over two decades, precisely because one formatted string is all it takes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check:&lt;/strong&gt; search for f", %, or .format( on the same line as execute(. Any hit is a red flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; pass values as parameters and let the driver escape them:&lt;/p&gt;

&lt;p&gt;from sqlalchemy import text&lt;/p&gt;

&lt;p&gt;email = request.args.get("email", "")&lt;br&gt;
result = db.session.execute(&lt;br&gt;
    text("SELECT * FROM users WHERE email = :email"),&lt;br&gt;
    {"email": email},&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;Or stay fully in the ORM, which is even simpler:&lt;/p&gt;

&lt;p&gt;user = User.query.filter_by(email=email).first()&lt;/p&gt;

&lt;p&gt;Rule of thumb: if you ever see an f-string inside a database query in AI-generated code, review the entire file. Where there is one, there are usually more.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Login endpoints anyone can hammer forever
&lt;/h2&gt;

&lt;p&gt;A typical generated login route looks fine:&lt;/p&gt;

&lt;p&gt;@app.route("/api/login", methods=["POST"])&lt;br&gt;
def login():&lt;br&gt;
    user = User.query.filter_by(email=request.json["email"]).first()&lt;br&gt;
    if user and check_password(request.json["password"], user.password_hash):&lt;br&gt;
        return jsonify(token=create_token(user))&lt;br&gt;
    return jsonify(error="invalid credentials"), 401&lt;/p&gt;

&lt;p&gt;The problem is what it doesn't do: it lets anyone try passwords as fast as their machine can send requests. No limit, no slowdown, no lockout. Weak passwords fall in hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check:&lt;/strong&gt; look at your login endpoint and ask a simple question. What stops someone from sending ten thousand requests? If the answer is "nothing," that is the hole.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; rate limit the sensitive endpoints. Flask-Limiter makes it a two-line change:&lt;/p&gt;

&lt;p&gt;from flask_limiter import Limiter&lt;br&gt;
from flask_limiter.util import get_remote_address&lt;/p&gt;

&lt;p&gt;limiter = Limiter(get_remote_address, app=app, storage_uri="redis://localhost:6379")&lt;/p&gt;

&lt;p&gt;@app.route("/api/login", methods=["POST"])&lt;br&gt;
@limiter.limit("5 per minute")&lt;br&gt;
def login():&lt;br&gt;
    ...&lt;/p&gt;

&lt;p&gt;Five attempts per minute per IP turns "try a million passwords tonight" into "try a few thousand over days." Most attackers just move on. Do the same for registration, password reset, and any endpoint that sends emails or SMS. Those are abuse magnets too. And make sure passwords are stored with bcrypt or argon2, via Werkzeug's generate_password_hash, never plain text or fast hashes like MD5.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. No security headers at all
&lt;/h2&gt;

&lt;p&gt;Open your browser's developer tools, go to the Network tab, and click any response from your app. If you don't see headers like Content-Security-Policy or Strict-Transport-Security, your app is telling browsers "trust everything, embed me anywhere."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's dangerous:&lt;/strong&gt; without a Content-Security-Policy, one reflected XSS, meaning a malicious script sneaked in through user input or a crafted link, runs with full access to your users' sessions. Without X-Frame-Options, an attacker can embed your app inside their own page and trick users into clicking things they can't see. That is clickjacking. Flask deliberately does not set these headers for you, and generators quietly skip the step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; one extension, about ten lines:&lt;/p&gt;

&lt;p&gt;from flask_talisman import Talisman&lt;/p&gt;

&lt;p&gt;Talisman(&lt;br&gt;
    app,&lt;br&gt;
    content_security_policy={&lt;br&gt;
        "default-src": "'self'",&lt;br&gt;
        "script-src": "'self'",&lt;br&gt;
        "style-src": "'self'",&lt;br&gt;
    },&lt;br&gt;
    force_https=False,  # let your reverse proxy (Nginx/Cloudflare) handle HTTPS&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;If your app legitimately needs external scripts like analytics or payment widgets, add those specific domains to the policy instead of disabling it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger picture
&lt;/h2&gt;

&lt;p&gt;None of this is really the AI's fault. Generators optimize for "does it run and does it look right." Security is invisible until the day it fails, so it gets skipped by default. Every generated app needs one deliberate hardening pass before real users and real data touch it. One hour, one checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No debug=True anywhere near production&lt;/li&gt;
&lt;li&gt;No secrets in code. Environment variables only, and rotate anything that ever leaked&lt;/li&gt;
&lt;li&gt;No f-strings or string formatting inside database queries&lt;/li&gt;
&lt;li&gt;Rate limits on login, registration, and reset endpoints&lt;/li&gt;
&lt;li&gt;Security headers via Flask-Talisman&lt;/li&gt;
&lt;li&gt;Bonus: passwords hashed with bcrypt or argon2, and dependencies scanned regularly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My rule of thumb, and the mindset behind everything we ship at Zyntari: treat AI-generated code like code from a fast, talented junior developer who has never been on call during a breach. A great starting point, never the finished product. Review the auth, review the queries, review the secrets. Then ship.&lt;/p&gt;




&lt;p&gt;Alon Trifonov is the founder of Zyntari (zyntari.com), an independent engineering studio in Israel that turns prototypes and AI-generated apps into secure, production-grade software. He builds with Python, Flask, and React, with security as a default rather than an afterthought.&lt;/p&gt;

</description>
      <category>flask</category>
      <category>python</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
