Introduction
SQL injection has topped security vulnerability charts for decades — and for good reason. It's one of those vulnerabilities that's embarrassingly easy to introduce and potentially catastrophic to leave unpatched. Despite being well-understood, it continues to appear in codebases everywhere, from hobby projects to enterprise software.
This post examines a real SQL injection vulnerability discovered and patched in DBeaver, a popular open-source database management tool. The vulnerability lived in the Altibase database extension and involved a seemingly harmless use of String.format() to build a SQL query. Let's dig in.
The Vulnerability Explained
What Went Wrong?
The vulnerable code was located in AltibaseMetaModel.java, inside a method called getDepDDLFromDbmsMetadata. This method retrieves dependent DDL (Data Definition Language) statements from the Altibase database engine using the DBMS_METADATA package.
Here's the problematic code:
// VULNERABLE CODE
String getDepDdlQry = "SELECT dbms_metadata.get_dependent_ddl('%s', '%s', '%s') FROM DUAL";
Statement stmt = conn.createStatement();
rs = stmt.executeQuery(
String.format(getDepDdlQry, depObjectType, sourceObject.getName(), schemaName)
);
At first glance, this looks like a straightforward query. But look closely: three user-influenced values — depObjectType, sourceObject.getName(), and schemaName — are being interpolated directly into the SQL string using String.format().
This means the final SQL string is constructed before it ever reaches the database. If any of those values contain SQL metacharacters, they become part of the query's logic, not just its data.
How Could It Be Exploited?
Imagine a schema name or object name that contains a single quote or a SQL comment sequence. An attacker (or even a maliciously named database object) could craft a value like:
schemaName = "PUBLIC', 'MALICIOUS_TYPE') -- "
The resulting query would become:
SELECT dbms_metadata.get_dependent_ddl('SOME_TYPE', 'SOME_OBJECT', 'PUBLIC', 'MALICIOUS_TYPE') -- ') FROM DUAL
The comment sequence (--) causes the rest of the original query to be ignored. Depending on the database's stored procedures and permissions, this could be leveraged to:
- Exfiltrate sensitive metadata from the database catalog
- Cause unexpected procedure calls with attacker-controlled arguments
- Bypass access controls enforced at the application layer
- Trigger errors that leak internal database structure information
Real-World Impact
In the context of a database management tool like DBeaver, this is particularly sensitive. The tool often connects to databases with elevated privileges — DBA accounts, schema owners, or administrative users. A successful injection in this context could have a blast radius far larger than the same vulnerability in a typical web application.
Additionally, because this code path is triggered when browsing database objects, the attack surface includes any scenario where an attacker can influence object names — for example, in shared database environments or when opening a database file provided by an untrusted source.
Severity Note: While this vulnerability is rated low severity (due to the specific conditions required to exploit it), SQL injection vulnerabilities should never be dismissed. The "low" rating reflects exploitability constraints, not the potential damage if those constraints are met.
The Fix
What Changed?
The fix is clean, minimal, and follows industry best practices. Here's the patched code:
// FIXED CODE
String getDepDdlQry = "SELECT dbms_metadata.get_dependent_ddl(?, ?, ?) FROM DUAL";
PreparedStatement pstmt = conn.prepareStatement(getDepDdlQry);
pstmt.setString(1, depObjectType);
pstmt.setString(2, sourceObject.getName());
pstmt.setString(3, schemaName);
rs = pstmt.executeQuery();
Before vs. After
| Aspect | Before (Vulnerable) | After (Fixed) |
|---|---|---|
| Query construction | String.format() with %s placeholders |
SQL ? parameter placeholders |
| Statement type | Statement |
PreparedStatement |
| Input handling | Interpolated into SQL string | Passed as typed parameters |
| Injection risk | ✅ Present | ❌ Eliminated |
Why Does This Fix Work?
Parameterized queries (also called prepared statements) work by separating the SQL code from the data. When you write:
String getDepDdlQry = "SELECT dbms_metadata.get_dependent_ddl(?, ?, ?) FROM DUAL";
PreparedStatement pstmt = conn.prepareStatement(getDepDdlQry);
The SQL structure is sent to the database first, and the database parses and compiles it as a query template. The ? placeholders are slots for data — nothing more. When you subsequently call:
pstmt.setString(1, depObjectType);
The JDBC driver ensures the value is properly escaped and transmitted as a string literal, not as SQL syntax. No matter what depObjectType contains — single quotes, semicolons, comment sequences — it will always be treated as a string value, never as SQL code.
This is fundamentally different from string formatting, where the database sees a single pre-built string and has no way to distinguish between "code the developer wrote" and "data the user provided."
Key Takeaways
Let's recap what we learned from this vulnerability and its fix:
-
String.format()and SQL don't mix. Whenever you see SQL being built with string interpolation, treat it as a red flag. -
PreparedStatementis the right tool. In Java, there's almost never a good reason to use a rawStatementwhen executing queries with external data. -
"Internal" code paths need security review too. This vulnerability wasn't in a login form or a search box — it was in a metadata retrieval utility. Security doesn't stop at the "user-facing" boundary.
-
Automated tools catch real bugs. This fix was identified through automated static analysis, demonstrating the value of integrating security scanning into your development workflow.
-
The fix was simple. Ten lines of code changed, zero functionality lost, one vulnerability eliminated. Security improvements don't always require architectural overhauls — sometimes they're just about using the right API.
Conclusion
SQL injection is a vulnerability with a simple root cause and an equally simple cure: never trust string concatenation or formatting for SQL construction; always use parameterized queries. The vulnerability fixed here in DBeaver's Altibase extension is a textbook example of how easy it is to make this mistake, and how straightforward it is to correct.
As developers, we owe it to our users and our systems to treat every database query as a potential attack surface. The good news is that the tools to write safe SQL have existed for decades — we just need to use them consistently.
If you're working with JDBC in Java, make PreparedStatement your default. If you're using another language or framework, learn its equivalent parameterization mechanism. Your future self — and your security team — will thank you.
Found a vulnerability in your codebase? Automated security scanning can help identify issues like this one before they reach production. Consider integrating tools like Semgrep, SonarQube, or GitHub's CodeQL into your CI/CD pipeline.