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
•Technically reviewed by Anupam Mediratta•Published April 30, 2026•Reviewed 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

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.


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.

Prevention and further reading

Related Articles

critical

Go ArticleRemove() SQL Injection via fmt.Sprintf IN Clause

The Go backend's `ArticleRemove` function built a SQL `DELETE` statement by joining a caller-supplied slice of article IDs directly into the query string with `fmt.Sprintf`. Because the `aids` values came straight from the frontend with no validation or escaping, an attacker could inject arbitrary SQL into the `IN(...)` clause. The fix replaces string concatenation with a parameterized query using placeholders and a `DB.Exec` argument list.

critical

JdbcSinkFunction.invoke() SQL Injection via Unvalidated Identifiers

The `JdbcSinkFunction.invoke()` method in Lacus's RTC engine built SQL INSERT statements with `String.format()`, placing database name, table name, and column names directly into the query string without validation. Although the actual row values used parameterized `?` placeholders, the identifiers themselves were injectable, letting a crafted sink configuration break out of the intended INSERT statement. The fix adds a strict allowlist regex that rejects any identifier containing characters out

critical

Node.js Auth Query SQL Injection via Group Code Interpolation

A sign-in authorization check built its SQL query by mapping an `authorizedGroups` array into quoted string literals and joining them directly into a template literal, creating a classic SQL injection point in a critical authentication path. The fix replaces every interpolated value — including the previously "typed" parameter — with `?` placeholders bound through a value-builder helper, closing off the injection vector entirely.

critical

Toolforge Database Creation SQL Injection via Unicode Backtick Bypass

A critical SQL injection vulnerability in the database creation routine of a Node.js wiki library allowed attackers to inject arbitrary SQL by exploiting a weak validation check. The fix replaces a character-based block with a strict allowlist pattern, ensuring only valid database identifiers reach the SQL engine.

critical

heatmap.php SQL Injection: $_REQUEST Parameters in Unparameterized

A critical SQL injection vulnerability in the heatmap data retrieval endpoint allowed attackers to execute arbitrary database commands by manipulating coordinate bounds or time range parameters. The vulnerability affected all six user-controlled $_REQUEST parameters passed directly into query construction without parameterization.

critical

Actual Budget addTransaction.sh SQL Injection via Shell Variable

A critical SQL injection vulnerability in Actual Budget's transaction automation script allowed attackers to manipulate database records through shell variables interpolated directly into SQL strings. The fix introduces proper escaping functions and numeric validation to prevent injection through unquoted fields.