Vibe Coding and Security Vulnerability
Explore the hidden security risks of "vibe coding" and rapid AI-assisted development. This hands-on breakdown demonstrates how a simple Python f-string can leave a database vulnerable to a catastrophic SQL injection attack, and how implementing bound parameters in SQLAlchemy draws a hard line between executable logic and untrusted user data.
I’ve finally gotten a clearer picture of SQL injection by actually getting my hands dirty.
Instead of just reading the theory, I ran a "drill" to experience exactly how to detect the vulnerability and apply the patch. In the current era of "vibe coding," where we often rely on AI-generated boilerplate to move fast, it’s dangerously easy to overlook how a single f-string can compromise an entire database.
The Experiment: Breaking My Own DB
I spun up a local PostgreSQL database and wrote a FastAPI endpoint that was "vulnerable on purpose."
I used Python string interpolation (f-strings) to merge user data directly into the SQL logic. To the naked eye, it looks like a clean shortcut. To a hacker, it’s a wide-open door.
user_input = "Moe"
DANGER: (Merging data and logic into one string)
query = text(f"INSERT INTO tableOne (ColA, ColB, ColC) VALUES ('{user_input}', 12, True)")
The "Hacker Injecting Part":
I acted as the attacker and entered this as my username:
' );DROP TABLE tableOne; --
By using the ' to close the string and the -- to comment out the rest of my original code, I successfully "injected" a brand-new command. The database didn't just add a user; it executed the DROP TABLE command and wiped my data instantly.
The Patch: Bound Parameters
The fix isn't about complex sanitization or blacklisting characters. It’s about separation of concerns.
I moved to Bound Parameters in SQLAlchemy. This creates a "hard wall" between the command logic and the user data.
THE SECURE PATCH
Step 1: Use a placeholder (:val)
stmt = text("INSERT INTO tableOne (ColA, ColB, ColC) VALUES (:val, 12, True)")
with engine.connect() as db_connection:
try:
# Step 2: Pass data as a separate dictionary.
# The engine treats this strictly as data, never as executable code.
db_connection.execute(stmt, {"val": user_input})
db_connection.commit()
except Exception:
db_connection.rollback()
raise
The Takeaway
Think of your SQL query like a train track:
F-strings let the user lay down their own rails, potentially diverting your train into a wreck.
Bound parameters put the user in a locked passenger car. They can ride along, but they can't touch the controls.
In the age of AI-assisted coding, don't just "vibe" with the output. Understand the boundaries. Stay curious, but keep your databases safe!
