SQL Injection via String Formatting: How Parameterized Queries Save the Day
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."
Prevention & Best Practices
1. Always Use Parameterized Queries
This is the golden rule. Whenever you're incorporating external data into a SQL query, use parameterized queries or prepared statements. This applies to:
- User input from forms or APIs
- Values read from files or external systems
- Data retrieved from other database queries
- Object names, schema names, and identifiers passed between systems
// ❌ Never do this
String query = "SELECT * FROM " + tableName + " WHERE id = " + userId;
// ✅ Always do this
String query = "SELECT * FROM users WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(query);
pstmt.setInt(1, userId);
2. Be Extra Careful with Metadata Queries
Queries involving database metadata (schema names, object names, DDL operations) are often overlooked in security reviews because they feel "internal." But as this vulnerability demonstrates, these code paths can be just as dangerous as user-facing query builders.
3. Use an ORM or Query Builder
Object-Relational Mappers (ORMs) like Hibernate, jOOQ, or Spring Data JPA handle parameterization automatically in most cases. They reduce the surface area for injection vulnerabilities by abstracting raw SQL construction.
// jOOQ example - parameterization handled automatically
Result<Record> result = create
.select()
.from(TABLE)
.where(SCHEMA_NAME.eq(schemaName))
.fetch();
4. Conduct Regular Code Reviews with Security Focus
SQL injection vulnerabilities often slip through because they look correct. Establishing a checklist for code reviews that specifically calls out:
- Any use of
String.format(),StringBuilder, or+concatenation in SQL contexts - Any use of
Statementinstead ofPreparedStatement - Any query construction involving external or user-influenced data
5. Use Static Analysis Tools
Tools like Semgrep, SonarQube, SpotBugs with FindSecBugs, and Checkmarx can automatically detect SQL injection patterns during CI/CD pipelines. The rule that caught this vulnerability (insecure-document-method) is an example of exactly this kind of automated detection.
# Example Semgrep rule concept for Java SQL injection
rules:
- id: java-sql-injection-string-format
pattern: |
String $QUERY = String.format($TEMPLATE, ...);
$STMT.executeQuery($QUERY);
message: Potential SQL injection via string formatting
severity: WARNING
6. Follow Security Standards
This vulnerability maps to well-established security standards:
- OWASP Top 10 (2021): A03 - Injection — SQL injection remains one of the most critical web application security risks
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command — The canonical classification for SQL injection
- SANS Top 25 — SQL injection consistently appears on this list of most dangerous software errors
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.