Back to Blog
high SEVERITY8 min read

SQL Injection in OceanBase Connector: How f-Strings Can Sink Your RAG Platform

A critical SQL injection vulnerability was discovered and patched in the OceanBase database connector used by a RAG (Retrieval-Augmented Generation) platform, where user-controlled filter expressions were directly embedded into SQL WHERE clauses using Python f-strings without any parameterization or validation. This flaw exposed the platform's entire knowledge base to complete compromise, including unauthorized data access, modification, and deletion. The fix replaces unsafe string interpolation

O
By Orbis AppSec
Published April 30, 2026Reviewed June 3, 2026

Answer Summary

This is a SQL injection vulnerability (CWE-89) in a Python OceanBase database connector used by a RAG platform. The root cause was using Python f-strings to directly embed user-controlled filter expressions into SQL WHERE clauses without parameterization or validation. The fix replaces unsafe string interpolation with parameterized queries, ensuring user input is properly escaped and treated as data rather than executable SQL code. This prevents attackers from injecting malicious SQL commands that could extract, modify, or delete the entire knowledge base.

Vulnerability at a Glance

cweCWE-89
fixReplace string interpolation with parameterized queries using proper SQL escaping
riskComplete database compromise including unauthorized data access, modification, and deletion
languagePython
root causeUser-controlled filter expressions embedded directly into SQL WHERE clauses using f-strings
vulnerabilitySQL Injection via f-string interpolation

SQL Injection in OceanBase Connector: How f-Strings Can Sink Your RAG Platform

Severity: Critical | CVE Class: SQL Injection (CWE-89) | File: rag/utils/ob_conn.py


Introduction

There's a reason SQL injection has sat at the top of the OWASP Top 10 for over two decades. It's deceptively simple to introduce, devastatingly effective to exploit, and surprisingly easy to miss during code review — especially when it's hiding inside a modern Python f-string.

This post walks through a critical SQL injection vulnerability that was recently discovered and patched in the OceanBase database connector (ob_conn.py) of a Retrieval-Augmented Generation (RAG) platform. If you're building AI-powered applications backed by a knowledge base, this one should get your full attention.


What Is This Vulnerability?

At its core, this is a SQL injection vulnerability — specifically, one where user-controlled filter expressions were embedded directly into SQL WHERE clauses using Python f-strings. No parameterization. No allowlist validation. No escaping. Just raw user input flowing straight into a live database query.

The vulnerability was identified at multiple locations across ob_conn.py (lines 726, 777, 781, 787, 793, 821, and 827), meaning it wasn't an isolated mistake — it was a systemic pattern repeated throughout the file.

Why Should Developers Care?

If you're building a RAG application, your vector database or document store is your product. It contains the curated knowledge that powers your AI's responses. A SQL injection vulnerability here doesn't just leak a user table — it exposes your entire knowledge base to:

  • Unauthorized data exfiltration (an attacker reads everything)
  • Data manipulation (an attacker poisons your AI's knowledge)
  • Data destruction (an attacker deletes your knowledge base)
  • Privilege escalation (depending on database configuration)

This is why the security scanner flagged it as the most critical vulnerability in the codebase — it directly targets the platform's core business asset.


The Vulnerability Explained

Technical Details

The vulnerable pattern looks something like this:

# VULNERABLE CODE — Do not use this pattern
def search_documents(self, filter_expression: str, limit: int = 10):
    # filter_expression comes from user input or API response
    query = f"""
        SELECT * FROM documents
        WHERE {filter_expression}
        LIMIT {limit}
    """
    cursor.execute(query)
    return cursor.fetchall()

On the surface, this looks clean and Pythonic. The f-string is concise, readable, and feels natural to write. But here's the problem: filter_expression is a string that an attacker controls.

When you embed user input directly into a SQL statement, you're trusting the user to behave like a developer. They won't.

How Could It Be Exploited?

Consider what happens when an attacker passes this as a filter_expression:

1=1 UNION SELECT username, password, NULL FROM admin_users --

The resulting query becomes:

SELECT * FROM documents
WHERE 1=1 UNION SELECT username, password, NULL FROM admin_users --
LIMIT 10

The -- comments out the rest of the query. The UNION SELECT appends results from the admin_users table. The attacker now has credentials.

Or consider a destructive payload:

1=1; DROP TABLE documents; --

Which becomes:

SELECT * FROM documents
WHERE 1=1; DROP TABLE documents; --
LIMIT 10

Your entire knowledge base: gone.

Real-World Attack Scenario

Here's how an attack might unfold in practice:

  1. Reconnaissance: An attacker discovers the RAG platform accepts filter parameters through its API (e.g., a document search endpoint).

  2. Injection Testing: They submit a simple test payload like ' OR '1'='1 and observe whether the response changes — it does.

  3. Data Exfiltration: Using a tool like sqlmap or manual UNION-based injection, they enumerate table names, then systematically dump the entire knowledge base.

  4. Persistence or Destruction: Depending on their goal, they either exfiltrate the data quietly over time, or — if the database user has write permissions — they modify or delete records to corrupt the AI's outputs.

  5. The AI Becomes a Weapon: If the attacker can write to the knowledge base, they can inject malicious content that the RAG system will confidently serve to end users. This is a prompt injection via database poisoning — a particularly nasty second-order attack.

The Scope of the Problem

What makes this especially concerning is that the pattern appeared seven times across the same file. This suggests the vulnerability was introduced as a design pattern rather than a one-off oversight, meaning every query builder function that accepted filter expressions was affected.


The Fix

What Changed?

The fix replaces unsafe f-string interpolation with parameterized queries (also called prepared statements). This is the gold-standard defense against SQL injection.

Here's the before/after comparison:

Before (Vulnerable)

# BEFORE: Direct f-string interpolation — UNSAFE
def get_chunks_by_filter(self, filter_expr: str):
    query = f"SELECT * FROM chunks WHERE {filter_expr}"
    self.cursor.execute(query)
    return self.cursor.fetchall()

def delete_by_filter(self, filter_expr: str):
    query = f"DELETE FROM documents WHERE {filter_expr}"
    self.cursor.execute(query)
    self.conn.commit()

After (Fixed)

# AFTER: Parameterized queries with allowlist validation — SAFE
ALLOWED_FILTER_FIELDS = {"doc_id", "kb_id", "status", "created_at"}

def get_chunks_by_filter(self, doc_id: str, kb_id: str):
    # Use placeholders — the database driver handles escaping
    query = "SELECT * FROM chunks WHERE doc_id = %s AND kb_id = %s"
    self.cursor.execute(query, (doc_id, kb_id))
    return self.cursor.fetchall()

def delete_by_filter(self, doc_id: str):
    # Parameters are passed separately, never interpolated into the string
    query = "DELETE FROM documents WHERE doc_id = %s"
    self.cursor.execute(query, (doc_id,))
    self.conn.commit()

How Does This Solve the Problem?

The key insight is separation of code and data.

With f-strings, the user's input becomes part of the SQL statement itself. The database parser sees it as SQL code and executes it accordingly.

With parameterized queries, the SQL statement is sent to the database first as a template with placeholders (%s or ?). The user's values are sent separately as data. The database engine knows these are values, not code — no matter what characters they contain.

An attacker passing ' OR '1'='1 as a parameter value would cause the database to literally search for a doc_id equal to the string ' OR '1'='1 — which won't match anything. The injection attempt fails harmlessly.

Additional Defense: Allowlist Validation

For cases where dynamic field names are genuinely needed (e.g., sorting by a user-selected column), the fix also applies allowlist validation:

# Allowlist approach for dynamic field names (column names can't be parameterized)
SORTABLE_FIELDS = {"created_at", "updated_at", "doc_name", "relevance_score"}

def get_documents_sorted(self, sort_field: str, limit: int):
    if sort_field not in SORTABLE_FIELDS:
        raise ValueError(f"Invalid sort field: {sort_field}")

    # Only safe, pre-approved field names reach the query
    query = f"SELECT * FROM documents ORDER BY {sort_field} LIMIT %s"
    self.cursor.execute(query, (limit,))
    return self.cursor.fetchall()

Note that column/table names cannot be parameterized in most database drivers — this is where allowlists become essential.


Prevention & Best Practices

1. Always Use Parameterized Queries

This is non-negotiable. Every major database driver supports them:

# MySQL / OceanBase (mysql-connector-python)
cursor.execute("SELECT * FROM t WHERE id = %s", (user_id,))

# SQLite
cursor.execute("SELECT * FROM t WHERE id = ?", (user_id,))

# PostgreSQL (psycopg2)
cursor.execute("SELECT * FROM t WHERE id = %s", (user_id,))

# SQLAlchemy ORM (parameterization is automatic)
session.query(Document).filter(Document.id == user_id).all()

2. Use an ORM

Object-Relational Mappers like SQLAlchemy, Django ORM, or Tortoise ORM handle parameterization automatically. They're not a silver bullet, but they dramatically reduce the surface area for injection:

# SQLAlchemy — safe by default
documents = session.query(Document).filter(
    Document.kb_id == kb_id,
    Document.status == "active"
).all()

3. Apply the Principle of Least Privilege

Your database user should only have the permissions it needs:

  • A read-only search function → use a read-only DB user
  • Never use root or admin DB credentials in application code
  • Separate DB users for read vs. read/write operations

4. Input Validation (Defense in Depth)

Validate inputs before they reach the database layer:

import re

def validate_kb_id(kb_id: str) -> bool:
    # Only allow alphanumeric IDs and hyphens
    return bool(re.match(r'^[a-zA-Z0-9\-]{1,64}$', kb_id))

5. Enable SQL Query Logging in Development

Seeing the actual queries your application generates is invaluable:

# SQLAlchemy query logging
import logging
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)

6. Use Static Analysis Tools

Integrate security scanning into your CI/CD pipeline:

  • Bandit — Python-specific security linter, flags SQL injection patterns
  • Semgrep — Highly customizable static analysis with SQL injection rules
  • SonarQube — Enterprise-grade SAST with injection detection
  • CodeQL — GitHub's semantic code analysis engine
# Run Bandit on your codebase
pip install bandit
bandit -r rag/ -t B608  # B608 = SQL injection

7. Security Code Review Checklist

When reviewing database-related code, ask:

  • [ ] Are all user inputs passed as parameters, not interpolated?
  • [ ] Are dynamic identifiers (column/table names) validated against an allowlist?
  • [ ] Does the DB user have only the minimum required permissions?
  • [ ] Are error messages suppressed in production (to avoid leaking schema info)?
  • [ ] Is there logging/alerting for unusual query patterns?

Relevant Standards & References

Standard Reference
OWASP Top 10 A03:2021 – Injection
CWE CWE-89: SQL Injection
OWASP Cheat Sheet SQL Injection Prevention
NIST SP 800-53: SI-10 Information Input Validation

Conclusion

This vulnerability is a textbook reminder that modern syntax doesn't mean safe syntax. Python f-strings are elegant and powerful — but when they carry user input into a SQL statement, they're just as dangerous as the string concatenation patterns we've been warning against for 20 years.

The fix here isn't complicated. Parameterized queries have been the right answer since the 1990s, and every modern database driver supports them. The challenge is building team habits and code review processes that catch these patterns before they reach production.

Key Takeaways

  • 🚨 Never interpolate user input into SQL strings — not with f-strings, not with .format(), not with + concatenation
  • Always use parameterized queries — they're the only reliable defense against SQL injection
  • 🔒 Apply least privilege — limit what damage a successful injection can do
  • 🔍 Scan your codebase — tools like Bandit and Semgrep can find these patterns automatically
  • 📋 Validate allowlists for any case where dynamic identifiers are unavoidable

In RAG applications especially, your knowledge base is your crown jewel. Protect the queries that touch it with the same rigor you'd apply to any other critical security boundary.


This vulnerability was identified and patched as part of an automated security review. The fix was verified by re-scan and LLM-assisted code review before merging.

Found a security issue in your codebase? Consider integrating automated security scanning into your CI/CD pipeline to catch vulnerabilities before they reach production.

Frequently Asked Questions

What is SQL injection in Python f-strings?

SQL injection via f-strings occurs when user-controlled data is directly interpolated into SQL queries using Python's f-string syntax (f"SELECT * FROM table WHERE {user_input}"), allowing attackers to inject malicious SQL commands that execute with the application's database privileges.

How do you prevent SQL injection in Python database connectors?

Use parameterized queries with placeholder syntax (e.g., "WHERE column = %s" with separate parameter values), validate and sanitize all user input, implement allowlists for dynamic column names, and never use string concatenation or f-strings to build SQL queries with user-controlled data.

What CWE is SQL injection?

SQL injection is classified as CWE-89 (Improper Neutralization of Special Elements used in an SQL Command). It's one of the most critical web application vulnerabilities and consistently appears in the OWASP Top 10.

Is input validation enough to prevent SQL injection?

No. While input validation is a defense-in-depth measure, parameterized queries are the primary defense. Validation alone is insufficient because attackers can often bypass filters with encoding tricks, and maintaining comprehensive blocklists is error-prone. Always use parameterized queries as the foundation.

Can static analysis detect SQL injection from f-strings?

Yes. Modern static analysis tools like Semgrep, Bandit, and CodeQL can detect SQL injection patterns where f-strings or string concatenation are used to build SQL queries. These tools can identify dangerous patterns before code reaches production.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #14470

Related Articles

critical

How SQL Injection happens in Python database scripts and how to fix it

A critical SQL injection vulnerability was discovered in `MangosSuperUI/Scripts/discover_relationships.py`, where database, table, and column names were interpolated directly into SQL queries using Python f-strings. An attacker controlling these input parameters could execute arbitrary SQL against the database. The fix applies backtick escaping for identifier names and parameterized queries for the `LIMIT` clause.

critical

How SQL Injection happens in Node.js SQLite CLI calls and how to fix it

A critical SQL injection vulnerability was discovered in `lib/ParamediciOSPermissions.js`, where the `service` and `app` variables were interpolated directly into raw SQL strings passed to the `sqlite3` command-line tool without any escaping or parameterization. An attacker with control over these inputs could manipulate the iOS simulator's TCC permission database, potentially granting unauthorized app permissions. The fix applies SQLite-standard single-quote escaping to both variables before th

critical

How SQL Injection happens in Python SQLite utilities and how to fix it

A SQL injection risk was discovered in `scripts/db_utils.py` where the `_get_or_create` function used f-string interpolation to dynamically construct table and column names in SQL queries. While current callers passed hardcoded values, the function accepted arbitrary strings, making it a latent injection vector for any future code that passed user-controlled input. The fix replaces dynamic SQL construction with a strict allowlist of pre-written, parameterized query strings.

critical

How Unsafe Fall-Through in getWhereConditions Happens in Sequelize and How to Fix It

A critical vulnerability in Sequelize (CVE-2023-22579) allowed attackers to inject raw SQL through an unsafe fall-through in the `getWhereConditions` function when parentheses were used in query attributes. Upgrading from version 6.26.0 to 6.29.0 closes this attack vector by tightening how raw attributes are handled. Any Node.js application using Sequelize for database queries should treat this upgrade as an urgent security priority.

high

How SQL Injection happens in Python BigQuery connectors and how to fix it

A high-severity SQL injection vulnerability was discovered in a BigQuery connector's query-building logic, where Python f-strings interpolated user-controlled identifiers—project_id, dataset_id, table_id, and timestamp_column—directly into SQL without validation. An attacker with control over connector configuration could inject arbitrary BigQuery SQL, including destructive statements. The fix introduces strict allowlist-based identifier validation using compiled regular expressions before any S

critical

How SQL Injection happens in PHP PDO queries and how to fix it

A critical SQL injection vulnerability was discovered in the `getOfficialContests()` method of ContestRepository.php, where the `$site_id` parameter was directly interpolated into a SQL query string instead of using prepared statements. This vulnerability allowed attackers to inject arbitrary SQL commands and potentially access or manipulate the entire contest database. The fix replaced `pdo->query()` with `pdo->prepare()` and proper parameter binding.