Feb 17, 2026·5 min read·6 visits
The JRuby database adapter, `activerecord-jdbc-adapter`, failed to safely handle SQL parameter substitution. Attackers can inject a `?` character into a field, which the adapter mistakenly treats as a new placeholder for the *next* value. This allows the second value to be injected inside the first value's quoted string, enabling SQL injection. Patch to version 1.2.8 immediately.
A critical SQL injection vulnerability in the ActiveRecord-JDBC-Adapter allows attackers to manipulate database queries via malformed bind parameters. By injecting specific characters into placeholder values, attackers can trigger a 'double substitution' event, effectively breaking out of quoted strings and executing arbitrary SQL commands on JRuby-based applications.
In the world of Ruby on Rails, we take certain things for granted. We assume ActiveRecord will protect us. We assume that when we use Model.where(name: params[:name]), the framework acts as an impenetrable shield against SQL injection. For the most part, we're right. But if you are running Rails on the Java Virtual Machine (JRuby), you rely on a bridge called activerecord-jdbc-adapter (AR-JDBC).
This library is the translator between Ruby's elegant active record pattern and Java's rigid JDBC drivers. It's a critical piece of infrastructure for enterprise Ruby apps. But for years, a logic bomb was ticking inside its string handling code—a flaw so fundamental it turns the concept of 'parameterized queries' on its head.
GHSA-5qw5-wf2q-f538 isn't just a failure of input sanitization; it's a failure of logic. It is a vulnerability that allows an attacker to confuse the database driver about what is code and what is data, simply by asking a question (mark).
To understand this bug, you have to understand how poor man's parameterized queries work. True prepared statements send the SQL template and the data separately to the database engine. But many adapters, for legacy or compatibility reasons, emulate this by taking the template (SELECT * FROM users WHERE name = ?) and the data (['Alice']), and mashing them together into a string before sending it over the wire.
The flaw in AR-JDBC lay in how it performed this mash-up. The adapter used a loop—specifically Ruby's gsub with a block—to find every ? and replace it with a quoted value. In theory, this is fine. In practice, the implementation suffered from a 'double substitution' or 'nested substitution' issue depending on the specific JRuby/Ruby version behaviors engaged.
When the adapter replaced the first ? with a user-supplied string, it didn't verify if that new string contained a ? of its own. If an attacker supplied Hacker? as the first parameter, the adapter inserted it. Then, due to the iterative nature of how the parameters were processed or subsequent passes in the driver chain, the engine would spot the ? inside the attacker's input and mistake it for the placeholder meant for the second parameter. It's the software equivalent of a snake eating its own tail, effectively allowing data to overwrite the structure of the query.
Let's look at the smoking gun in lib/arjdbc/jdbc/adapter.rb. The developers needed a way to swap binds into the SQL string. They chose this implementation:
# Vulnerable method in lib/arjdbc/jdbc/adapter.rb
def to_sql(sql, binds = [])
sql = sql.send(:to_sql) if sql.respond_to?(:to_sql)
return sql if binds.blank?
copy = binds.dup
# The critical failure:
sql.gsub('?') { quote(*copy.shift.reverse) }
endAt first glance, this looks like standard Ruby. gsub iterates over matches. copy.shift pulls the next value. quote makes it safe. Done, right? Wrong.
The issue is subtle. If copy.shift returns a string that contains a ?, and the to_sql method logic (or the surrounding infrastructure in older versions) allows for re-scanning or improper cursor management, the ? in the payload consumes the next item in copy.
This means the boundaries between separate bind parameters dissolve. Parameter A can consume Parameter B, injecting Parameter B's raw value into the middle of Parameter A's quoted string.
Let's weaponize this. Imagine a simple profile update query taking two parameters: a username and a bio.
Target Query:
UPDATE users SET username = ?, bio = ? WHERE id = 123;The Setup: We need two payloads.
?. Let's use bad_user?.', admin=1 --.The Execution:
? in the SQL template.'bad_user?'.? inside 'bad_user?'.The Resulting SQL:
UPDATE users SET username = 'bad_user', admin=1 --'', bio = ...See what happened? The injected ? acted as a portal. The second payload landed inside the string literal of the first, closing the quote (') early and injecting admin=1. The rest of the query (bio = ...) is commented out by the --. We just escalated privileges.
The fix for this is simple in principle but requires discipline: Do not use simple string replacement for SQL generation. The maintainers patched this in version 1.2.8.
The remediation strategy involves upgrading the gem. If you are stuck on a legacy version of JRuby or Rails, you are in dangerous waters. The patched versions likely move away from naive gsub usage or implement a scanner that is strictly position-aware, ensuring that replaced text is never eligible for future replacement passes.
> [!NOTE] > Developer Lesson: If you find yourself writing a regex to parse or modify SQL, stop. You are almost certainly creating a vulnerability. Use the tokenizer or parser provided by your database driver, or ensure your replacement logic consumes the template linearly without backtracking.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:P| Product | Affected Versions | Fixed Version |
|---|---|---|
activerecord-jdbc-adapter JRuby | < 1.2.8 | 1.2.8 |
| Attribute | Detail |
|---|---|
| Attack Vector | Network (Remote) |
| CVSS v4.0 | 8.8 (High) |
| CWE ID | CWE-89 (SQL Injection) |
| Platform | JRuby / Java |
| Component | lib/arjdbc/jdbc/adapter.rb |
| Exploit Status | PoC Available (Theoretical) |
Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')