VYPR
Moderate severityNVD Advisory· Published Oct 10, 2025· Updated Oct 10, 2025

Rack has Possible Information Disclosure Vulnerability

CVE-2025-61780

Description

Rack is a modular Ruby web server interface. Prior to versions 2.2.20, 3.1.18, and 3.2.3, a possible information disclosure vulnerability existed in Rack::Sendfile when running behind a proxy that supports x-sendfile headers (such as Nginx). Specially crafted headers could cause Rack::Sendfile to miscommunicate with the proxy and trigger unintended internal requests, potentially bypassing proxy-level access restrictions. When Rack::Sendfile received untrusted x-sendfile-type or x-accel-mapping headers from a client, it would interpret them as proxy configuration directives. This could cause the middleware to send a "redirect" response to the proxy, prompting it to reissue a new internal request that was not subject to the proxy's access controls. An attacker could exploit this by setting a crafted x-sendfile-type: x-accel-redirect header, setting a crafted x-accel-mapping header, and requesting a path that qualifies for proxy-based acceleration. Attackers could bypass proxy-enforced restrictions and access internal endpoints intended to be protected (such as administrative pages). The vulnerability did not allow arbitrary file reads but could expose sensitive application routes. This issue only affected systems meeting all of the following conditions: The application used Rack::Sendfile with a proxy that supports x-accel-redirect (e.g., Nginx); the proxy did not always set or remove the x-sendfile-type and x-accel-mapping headers; and the application exposed an endpoint that returned a body responding to .to_path. Users should upgrade to Rack versions 2.2.20, 3.1.18, or 3.2.3, which require explicit configuration to enable x-accel-redirect. Alternatively, configure the proxy to always set or strip the header, or in Rails applications, disable sendfile completely.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
rackRubyGems
< 2.2.202.2.20
rackRubyGems
>= 3.0, < 3.1.183.1.18
rackRubyGems
>= 3.2, < 3.2.33.2.3

Affected products

1

Patches

3
57277b774158

Improper handling of proxy headers in `Rack::Sendfile` may allow proxy bypass.

https://github.com/rack/rackSamuel WilliamsOct 7, 2025via ghsa
3 files changed · +207 50
  • CHANGELOG.md+20 0 modified
    @@ -2,6 +2,12 @@
     
     All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/).
     
    +## [3.2.3] - 2025-10-10
    +
    +### Security
    +
    +- [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
    +
     ## [3.2.2] - 2025-10-07
     
     ### Security
    @@ -67,6 +73,13 @@ This release continues Rack's evolution toward a cleaner, more efficient foundat
     - `SERVER_NAME` and `HTTP_HOST` are now more strictly validated according to the relevant specifications. ([#2298](https://github.com/rack/rack/pull/2298), [@ioquatix])
     - `Rack::Lint` now disallows `PATH_INFO="" SCRIPT_NAME=""`. ([#2298](https://github.com/rack/rack/issues/2307), [@jeremyevans])
     
    +## [3.1.18] - 2025-10-10
    +
    +### Security
    +
    +- [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
    +- [CVE-2025-61919](https://github.com/advisories/GHSA-6xw4-3v39-52mm) Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.
    +
     ## [3.1.17] - 2025-10-07
     
     ### Security
    @@ -444,6 +457,13 @@ This release introduces major improvements to Rack, including enhanced support f
     - Fix multipart filename generation for filenames that contain spaces. Encode spaces as "%20" instead of "+" which will be decoded properly by the multipart parser. ([#1736](https://github.com/rack/rack/pull/1645), [@muirdm](https://github.com/muirdm))
     - `Rack::Request#scheme` returns `ws` or `wss` when one of the `X-Forwarded-Scheme` / `X-Forwarded-Proto` headers is set to `ws` or `wss`, respectively. ([#1730](https://github.com/rack/rack/issues/1730), [@erwanst](https://github.com/erwanst))
     
    +## [2.2.20] - 2025-10-10
    +
    +### Security
    +
    +- [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
    +- [CVE-2025-61919](https://github.com/advisories/GHSA-6xw4-3v39-52mm) Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.
    +
     ## [2.2.19] - 2025-10-07
     
     ### Security
    
  • lib/rack/sendfile.rb+50 20 modified
    @@ -16,21 +16,21 @@ module Rack
       # delivery code.
       #
       # In order to take advantage of this middleware, the response body must
    -  # respond to +to_path+ and the request must include an x-sendfile-type
    +  # respond to +to_path+ and the request must include an `x-sendfile-type`
       # header. Rack::Files and other components implement +to_path+ so there's
    -  # rarely anything you need to do in your application. The x-sendfile-type
    +  # rarely anything you need to do in your application. The `x-sendfile-type`
       # header is typically set in your web servers configuration. The following
       # sections attempt to document
       #
       # === Nginx
       #
    -  # Nginx supports the x-accel-redirect header. This is similar to x-sendfile
    +  # Nginx supports the `x-accel-redirect` header. This is similar to `x-sendfile`
       # but requires parts of the filesystem to be mapped into a private URL
       # hierarchy.
       #
       # The following example shows the Nginx configuration required to create
    -  # a private "/files/" area, enable x-accel-redirect, and pass the special
    -  # x-sendfile-type and x-accel-mapping headers to the backend:
    +  # a private "/files/" area, enable `x-accel-redirect`, and pass the special
    +  # `x-accel-mapping` header to the backend:
       #
       #   location ~ /files/(.*) {
       #     internal;
    @@ -44,24 +44,29 @@ module Rack
       #     proxy_set_header   X-Real-IP           $remote_addr;
       #     proxy_set_header   X-Forwarded-For     $proxy_add_x_forwarded_for;
       #
    -  #     proxy_set_header   x-sendfile-type     x-accel-redirect;
       #     proxy_set_header   x-accel-mapping     /var/www/=/files/;
       #
       #     proxy_pass         http://127.0.0.1:8080/;
       #   }
       #
    -  # Note that the x-sendfile-type header must be set exactly as shown above.
    -  # The x-accel-mapping header should specify the location on the file system,
    +  # The `x-accel-mapping` header should specify the location on the file system,
       # followed by an equals sign (=), followed name of the private URL pattern
       # that it maps to. The middleware performs a simple substitution on the
       # resulting path.
       #
    +  # To enable `x-accel-redirect`, you must configure the middleware explicitly:
    +  #
    +  #   use Rack::Sendfile, "x-accel-redirect"
    +  #
    +  # For security reasons, the `x-sendfile-type` header from requests is ignored.
    +  # The sendfile variation must be set via the middleware constructor.
    +  #
       # See Also: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile
       #
       # === lighttpd
       #
    -  # Lighttpd has supported some variation of the x-sendfile header for some
    -  # time, although only recent version support x-sendfile in a reverse proxy
    +  # Lighttpd has supported some variation of the `x-sendfile` header for some
    +  # time, although only recent version support `x-sendfile` in a reverse proxy
       # configuration.
       #
       #   $HTTP["host"] == "example.com" {
    @@ -83,7 +88,7 @@ module Rack
       #
       # === Apache
       #
    -  # x-sendfile is supported under Apache 2.x using a separate module:
    +  # `x-sendfile` is supported under Apache 2.x using a separate module:
       #
       # https://tn123.org/mod_xsendfile/
       #
    @@ -97,16 +102,28 @@ module Rack
       # === Mapping parameter
       #
       # The third parameter allows for an overriding extension of the
    -  # x-accel-mapping header. Mappings should be provided in tuples of internal to
    +  # `x-accel-mapping` header. Mappings should be provided in tuples of internal to
       # external. The internal values may contain regular expression syntax, they
       # will be matched with case indifference.
    +  #
    +  # When `x-accel-redirect` is explicitly enabled via the variation parameter,
    +  # and no application-level mappings are provided, the middleware will read
    +  # the `x-accel-mapping` header from the proxy. This allows nginx to control
    +  # the path mapping without requiring application-level configuration.
    +  #
    +  # === Security
    +  #
    +  # For security reasons, the `x-sendfile-type` header from HTTP requests is
    +  # ignored. The sendfile variation must be explicitly configured via the
    +  # middleware constructor to prevent information disclosure vulnerabilities
    +  # where attackers could bypass proxy restrictions.
     
       class Sendfile
         def initialize(app, variation = nil, mappings = [])
           @app = app
           @variation = variation
           @mappings = mappings.map do |internal, external|
    -        [/^#{internal}/i, external]
    +        [/\A#{internal}/i, external]
           end
         end
     
    @@ -145,22 +162,35 @@ def call(env)
         end
     
         private
    +
         def variation(env)
    -      @variation ||
    -        env['sendfile.type'] ||
    -        env['HTTP_X_SENDFILE_TYPE']
    +      # Note: HTTP_X_SENDFILE_TYPE is intentionally NOT read for security reasons.
    +      # Attackers could use this header to enable x-accel-redirect and bypass proxy restrictions.
    +      @variation || env['sendfile.type']
    +    end
    +
    +    def x_accel_mapping(env)
    +      # Only allow header when:
    +      # 1. `x-accel-redirect` is explicitly enabled via constructor.
    +      # 2. No application-level mappings are configured.
    +      return nil unless @variation =~ /x-accel-redirect/i
    +      return nil if @mappings.any?
    +      
    +      env['HTTP_X_ACCEL_MAPPING']
         end
     
         def map_accel_path(env, path)
           if mapping = @mappings.find { |internal, _| internal =~ path }
    -        path.sub(*mapping)
    -      elsif mapping = env['HTTP_X_ACCEL_MAPPING']
    +        return path.sub(*mapping)
    +      elsif mapping = x_accel_mapping(env)
    +        # Safe to use header: explicit config + no app mappings:
             mapping.split(',').map(&:strip).each do |m|
               internal, external = m.split('=', 2).map(&:strip)
    -          new_path = path.sub(/^#{internal}/i, external)
    +          new_path = path.sub(/\A#{internal}/i, external)
               return new_path unless path == new_path
             end
    -        path
    +
    +        return path
           end
         end
       end
    
  • test/spec_sendfile.rb+137 30 modified
    @@ -22,12 +22,12 @@ def simple_app(body = sendfile_body)
         lambda { |env| [200, { 'content-type' => 'text/plain' }, body] }
       end
     
    -  def sendfile_app(body, mappings = [])
    -    Rack::Lint.new Rack::Sendfile.new(simple_app(body), nil, mappings)
    +  def sendfile_app(body, mappings = [], variation = nil)
    +    Rack::Lint.new Rack::Sendfile.new(simple_app(body), variation, mappings)
       end
     
    -  def request(headers = {}, body = sendfile_body, mappings = [])
    -    yield Rack::MockRequest.new(sendfile_app(body, mappings)).get('/', headers)
    +  def request(headers = {}, body = sendfile_body, mappings = [], variation = nil)
    +    yield Rack::MockRequest.new(sendfile_app(body, mappings, variation)).get('/', headers)
       end
     
       def open_file(path)
    @@ -48,7 +48,8 @@ def open_file(path)
     
       it "does nothing and logs to rack.errors when incorrect x-sendfile-type header present" do
         io = StringIO.new
    -    request 'HTTP_X_SENDFILE_TYPE' => 'X-Banana', 'rack.errors' => io do |response|
    +    # Configure with wrong variation type
    +    request({ 'rack.errors' => io }, sendfile_body, [], 'X-Banana') do |response|
           response.must_be :ok?
           response.body.must_equal 'Hello World'
           response.headers.wont_include 'x-sendfile'
    @@ -59,7 +60,7 @@ def open_file(path)
       end
     
       it "sets x-sendfile response header and discards body" do
    -    request 'HTTP_X_SENDFILE_TYPE' => 'x-sendfile' do |response|
    +    request({}, sendfile_body, [], 'X-Sendfile') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['content-length'].must_equal '0'
    @@ -71,7 +72,7 @@ def open_file(path)
         body = sendfile_body
         closed = false
         body.define_singleton_method(:close){closed = true}
    -    request({'HTTP_X_SENDFILE_TYPE' => 'x-sendfile'}, body) do |response|
    +    request({}, body, [], 'X-Sendfile') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['content-length'].must_equal '0'
    @@ -81,7 +82,7 @@ def open_file(path)
       end
     
       it "sets x-lighttpd-send-file response header and discards body" do
    -    request 'HTTP_X_SENDFILE_TYPE' => 'x-lighttpd-send-file' do |response|
    +    request({}, sendfile_body, [], 'X-Lighttpd-Send-File') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['content-length'].must_equal '0'
    @@ -91,10 +92,9 @@ def open_file(path)
     
       it "sets x-accel-redirect response header and discards body" do
         headers = {
    -      'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect',
           'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar/"
         }
    -    request headers do |response|
    +    request(headers, sendfile_body, [], 'X-Accel-Redirect') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['content-length'].must_equal '0'
    @@ -104,10 +104,9 @@ def open_file(path)
     
       it "sets x-accel-redirect response header to percent-encoded path" do
         headers = {
    -      'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect',
           'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar%/"
         }
    -    request headers, sendfile_body('file_with_%_?_symbol') do |response|
    +    request(headers, sendfile_body('file_with_%_?_symbol'), [], 'X-Accel-Redirect') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['content-length'].must_equal '0'
    @@ -116,7 +115,7 @@ def open_file(path)
       end
     
       it 'writes to rack.error when no x-accel-mapping is specified' do
    -    request 'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect' do |response|
    +    request({}, sendfile_body, [], 'X-Accel-Redirect') do |response|
           response.must_be :ok?
           response.body.must_equal 'Hello World'
           response.headers.wont_include 'x-accel-redirect'
    @@ -125,7 +124,7 @@ def open_file(path)
       end
     
       it 'does nothing when body does not respond to #to_path' do
    -    request({ 'HTTP_X_SENDFILE_TYPE' => 'x-sendfile' }, ['Not a file...']) do |response|
    +    request({}, ['Not a file...'], [], 'X-Sendfile') do |response|
           response.body.must_equal 'Not a file...'
           response.headers.wont_include 'x-sendfile'
         end
    @@ -147,14 +146,14 @@ def open_file(path)
             ["#{dir2}/", '/wibble/']
           ]
     
    -      request({ 'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect' }, first_body, mappings) do |response|
    +      request({}, first_body, mappings, 'X-Accel-Redirect') do |response|
             response.must_be :ok?
             response.body.must_be :empty?
             response.headers['content-length'].must_equal '0'
             response.headers['x-accel-redirect'].must_equal '/foo/bar/rack_sendfile'
           end
     
    -      request({ 'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect' }, second_body, mappings) do |response|
    +      request({}, second_body, mappings, 'X-Accel-Redirect') do |response|
             response.must_be :ok?
             response.body.must_be :empty?
             response.headers['content-length'].must_equal '0'
    @@ -181,34 +180,142 @@ def open_file(path)
           third_body = open_file(File.join(dir3, 'rack_sendfile'))
           third_body.puts 'hello again world'
     
    +      # Now we need to explicitly enable x-accel-redirect in the constructor
    +      app = Rack::Lint.new Rack::Sendfile.new(simple_app(first_body), "X-Accel-Redirect", [])
    +      
           headers = {
    -        'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect',
             'HTTP_X_ACCEL_MAPPING' => "#{dir1}/=/foo/bar/, #{dir2}/=/wibble/"
           }
     
    -      request(headers, first_body) do |response|
    +      response = Rack::MockRequest.new(app).get('/', headers)
    +      response.must_be :ok?
    +      response.body.must_be :empty?
    +      response.headers['content-length'].must_equal '0'
    +      response.headers['x-accel-redirect'].must_equal '/foo/bar/rack_sendfile'
    +
    +      app = Rack::Lint.new Rack::Sendfile.new(simple_app(second_body), "X-Accel-Redirect", [])
    +      response = Rack::MockRequest.new(app).get('/', headers)
    +      response.must_be :ok?
    +      response.body.must_be :empty?
    +      response.headers['content-length'].must_equal '0'
    +      response.headers['x-accel-redirect'].must_equal '/wibble/rack_sendfile'
    +
    +      app = Rack::Lint.new Rack::Sendfile.new(simple_app(third_body), "X-Accel-Redirect", [])
    +      response = Rack::MockRequest.new(app).get('/', headers)
    +      response.must_be :ok?
    +      response.body.must_be :empty?
    +      response.headers['content-length'].must_equal '0'
    +      response.headers['x-accel-redirect'].must_equal "#{dir3}/rack_sendfile"
    +    ensure
    +      FileUtils.remove_entry_secure dir1
    +      FileUtils.remove_entry_secure dir2
    +      FileUtils.remove_entry_secure dir3
    +    end
    +  end
    +
    +  # Security tests for CVE mitigation
    +  describe "security: information disclosure prevention" do
    +    it "ignores HTTP_X_SENDFILE_TYPE header to prevent attacker-controlled sendfile activation" do
    +      # Attacker tries to enable x-sendfile via header
    +      request 'HTTP_X_SENDFILE_TYPE' => 'x-sendfile' do |response|
             response.must_be :ok?
    -        response.body.must_be :empty?
    -        response.headers['content-length'].must_equal '0'
    -        response.headers['x-accel-redirect'].must_equal '/foo/bar/rack_sendfile'
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-sendfile'
           end
    +    end
     
    -      request(headers, second_body) do |response|
    +    it "ignores HTTP_X_SENDFILE_TYPE header attempting to enable x-accel-redirect" do
    +      # Attacker tries to enable x-accel-redirect via header with mapping
    +      headers = {
    +        'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect',
    +        'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/attacker/path/"
    +      }
    +      request headers do |response|
    +        response.must_be :ok?
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-accel-redirect'
    +      end
    +    end
    +
    +    it "ignores HTTP_X_ACCEL_MAPPING when x-accel-redirect is not explicitly enabled" do
    +      # Even if attacker sends mapping header, it should be ignored without explicit config
    +      headers = {
    +        'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/attacker/path/"
    +      }
    +      request headers do |response|
    +        response.must_be :ok?
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-accel-redirect'
    +      end
    +    end
    +
    +    it "ignores HTTP_X_ACCEL_MAPPING when application-level mappings are configured" do
    +      # When app provides mappings, header should be ignored for security
    +      begin
    +        dir = Dir.mktmpdir
    +        body = open_file(File.join(dir, 'rack_sendfile'))
    +        body.puts 'test'
    +        
    +        app_mappings = [["#{dir}/", '/app/mapping/']]
    +        app = Rack::Lint.new Rack::Sendfile.new(simple_app(body), "X-Accel-Redirect", app_mappings)
    +        
    +        headers = {
    +          'HTTP_X_ACCEL_MAPPING' => "#{dir}/=/attacker/path/"
    +        }
    +        
    +        response = Rack::MockRequest.new(app).get('/', headers)
             response.must_be :ok?
             response.body.must_be :empty?
    -        response.headers['content-length'].must_equal '0'
    -        response.headers['x-accel-redirect'].must_equal '/wibble/rack_sendfile'
    +        response.headers['x-accel-redirect'].must_equal '/app/mapping/rack_sendfile'
    +        response.headers['x-accel-redirect'].wont_equal '/attacker/path/rack_sendfile'
    +      ensure
    +        FileUtils.remove_entry_secure dir
           end
    +    end
     
    -      request(headers, third_body) do |response|
    +    it "allows HTTP_X_ACCEL_MAPPING only when x-accel-redirect explicitly enabled with no app mappings" do
    +      # This is the safe use case: explicit config + no app mappings = allow header
    +      begin
    +        dir = Dir.mktmpdir
    +        body = open_file(File.join(dir, 'rack_sendfile'))
    +        body.puts 'test'
    +        
    +        app = Rack::Lint.new Rack::Sendfile.new(simple_app(body), "X-Accel-Redirect", [])
    +        
    +        headers = {
    +          'HTTP_X_ACCEL_MAPPING' => "#{dir}/=/safe/nginx/mapping/"
    +        }
    +        
    +        response = Rack::MockRequest.new(app).get('/', headers)
             response.must_be :ok?
             response.body.must_be :empty?
    -        response.headers['content-length'].must_equal '0'
    -        response.headers['x-accel-redirect'].must_equal "#{dir3}/rack_sendfile"
    +        response.headers['x-accel-redirect'].must_equal '/safe/nginx/mapping/rack_sendfile'
    +      ensure
    +        FileUtils.remove_entry_secure dir
           end
    -    ensure
    -      FileUtils.remove_entry_secure dir1
    -      FileUtils.remove_entry_secure dir2
    +    end
    +
    +    it "does not allow x-lighttpd-send-file activation via header" do
    +      # Verify other sendfile types also can't be enabled via headers
    +      request 'HTTP_X_SENDFILE_TYPE' => 'x-lighttpd-send-file' do |response|
    +        response.must_be :ok?
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-lighttpd-send-file'
    +      end
    +    end
    +
    +    it "requires explicit middleware configuration for any sendfile variation" do
    +      # Test that sendfile.type env var still works (internal, not from HTTP headers)
    +      body = sendfile_body
    +      app = lambda { |env| [200, { 'content-type' => 'text/plain' }, body] }
    +      middleware = Rack::Lint.new Rack::Sendfile.new(app)
    +      
    +      env = Rack::MockRequest.env_for('/', { 'sendfile.type' => 'x-sendfile' })
    +      status, headers, response_body = middleware.call(env)
    +      
    +      status.must_equal 200
    +      headers['x-sendfile'].must_equal File.join(Dir.tmpdir, "rack_sendfile")
    +      headers['content-length'].must_equal '0'
         end
       end
     end
    
7e69f65eefe9

Improper handling of proxy headers in `Rack::Sendfile` may allow proxy bypass.

https://github.com/rack/rackSamuel WilliamsOct 7, 2025via ghsa
3 files changed · +200 50
  • CHANGELOG.md+13 0 modified
    @@ -2,6 +2,12 @@
     
     All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/).
     
    +## [3.1.18] - 2025-10-10
    +
    +### Security
    +
    +- [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
    +
     ## [3.1.17] - 2025-10-07
     
     ### Security
    @@ -373,6 +379,13 @@ Rack v3.1 is primarily a maintenance release that removes features deprecated in
     - Fix multipart filename generation for filenames that contain spaces. Encode spaces as "%20" instead of "+" which will be decoded properly by the multipart parser. ([#1736](https://github.com/rack/rack/pull/1645), [@muirdm](https://github.com/muirdm))
     - `Rack::Request#scheme` returns `ws` or `wss` when one of the `X-Forwarded-Scheme` / `X-Forwarded-Proto` headers is set to `ws` or `wss`, respectively. ([#1730](https://github.com/rack/rack/issues/1730), [@erwanst](https://github.com/erwanst))
     
    +## [2.2.20] - 2025-10-10
    +
    +### Security
    +
    +- [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
    +- [CVE-2025-61919](https://github.com/advisories/GHSA-6xw4-3v39-52mm) Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.
    +
     ## [2.2.19] - 2025-10-07
     
     ### Security
    
  • lib/rack/sendfile.rb+50 20 modified
    @@ -16,21 +16,21 @@ module Rack
       # delivery code.
       #
       # In order to take advantage of this middleware, the response body must
    -  # respond to +to_path+ and the request must include an x-sendfile-type
    +  # respond to +to_path+ and the request must include an `x-sendfile-type`
       # header. Rack::Files and other components implement +to_path+ so there's
    -  # rarely anything you need to do in your application. The x-sendfile-type
    +  # rarely anything you need to do in your application. The `x-sendfile-type`
       # header is typically set in your web servers configuration. The following
       # sections attempt to document
       #
       # === Nginx
       #
    -  # Nginx supports the x-accel-redirect header. This is similar to x-sendfile
    +  # Nginx supports the `x-accel-redirect` header. This is similar to `x-sendfile`
       # but requires parts of the filesystem to be mapped into a private URL
       # hierarchy.
       #
       # The following example shows the Nginx configuration required to create
    -  # a private "/files/" area, enable x-accel-redirect, and pass the special
    -  # x-sendfile-type and x-accel-mapping headers to the backend:
    +  # a private "/files/" area, enable `x-accel-redirect`, and pass the special
    +  # `x-accel-mapping` header to the backend:
       #
       #   location ~ /files/(.*) {
       #     internal;
    @@ -44,24 +44,29 @@ module Rack
       #     proxy_set_header   X-Real-IP           $remote_addr;
       #     proxy_set_header   X-Forwarded-For     $proxy_add_x_forwarded_for;
       #
    -  #     proxy_set_header   x-sendfile-type     x-accel-redirect;
       #     proxy_set_header   x-accel-mapping     /var/www/=/files/;
       #
       #     proxy_pass         http://127.0.0.1:8080/;
       #   }
       #
    -  # Note that the x-sendfile-type header must be set exactly as shown above.
    -  # The x-accel-mapping header should specify the location on the file system,
    +  # The `x-accel-mapping` header should specify the location on the file system,
       # followed by an equals sign (=), followed name of the private URL pattern
       # that it maps to. The middleware performs a simple substitution on the
       # resulting path.
       #
    +  # To enable `x-accel-redirect`, you must configure the middleware explicitly:
    +  #
    +  #   use Rack::Sendfile, "x-accel-redirect"
    +  #
    +  # For security reasons, the `x-sendfile-type` header from requests is ignored.
    +  # The sendfile variation must be set via the middleware constructor.
    +  #
       # See Also: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile
       #
       # === lighttpd
       #
    -  # Lighttpd has supported some variation of the x-sendfile header for some
    -  # time, although only recent version support x-sendfile in a reverse proxy
    +  # Lighttpd has supported some variation of the `x-sendfile` header for some
    +  # time, although only recent version support `x-sendfile` in a reverse proxy
       # configuration.
       #
       #   $HTTP["host"] == "example.com" {
    @@ -83,7 +88,7 @@ module Rack
       #
       # === Apache
       #
    -  # x-sendfile is supported under Apache 2.x using a separate module:
    +  # `x-sendfile` is supported under Apache 2.x using a separate module:
       #
       # https://tn123.org/mod_xsendfile/
       #
    @@ -97,16 +102,28 @@ module Rack
       # === Mapping parameter
       #
       # The third parameter allows for an overriding extension of the
    -  # x-accel-mapping header. Mappings should be provided in tuples of internal to
    +  # `x-accel-mapping` header. Mappings should be provided in tuples of internal to
       # external. The internal values may contain regular expression syntax, they
       # will be matched with case indifference.
    +  #
    +  # When `x-accel-redirect` is explicitly enabled via the variation parameter,
    +  # and no application-level mappings are provided, the middleware will read
    +  # the `x-accel-mapping` header from the proxy. This allows nginx to control
    +  # the path mapping without requiring application-level configuration.
    +  #
    +  # === Security
    +  #
    +  # For security reasons, the `x-sendfile-type` header from HTTP requests is
    +  # ignored. The sendfile variation must be explicitly configured via the
    +  # middleware constructor to prevent information disclosure vulnerabilities
    +  # where attackers could bypass proxy restrictions.
     
       class Sendfile
         def initialize(app, variation = nil, mappings = [])
           @app = app
           @variation = variation
           @mappings = mappings.map do |internal, external|
    -        [/^#{internal}/i, external]
    +        [/\A#{internal}/i, external]
           end
         end
     
    @@ -145,22 +162,35 @@ def call(env)
         end
     
         private
    +
         def variation(env)
    -      @variation ||
    -        env['sendfile.type'] ||
    -        env['HTTP_X_SENDFILE_TYPE']
    +      # Note: HTTP_X_SENDFILE_TYPE is intentionally NOT read for security reasons.
    +      # Attackers could use this header to enable x-accel-redirect and bypass proxy restrictions.
    +      @variation || env['sendfile.type']
    +    end
    +
    +    def x_accel_mapping(env)
    +      # Only allow header when:
    +      # 1. `x-accel-redirect` is explicitly enabled via constructor.
    +      # 2. No application-level mappings are configured.
    +      return nil unless @variation =~ /x-accel-redirect/i
    +      return nil if @mappings.any?
    +      
    +      env['HTTP_X_ACCEL_MAPPING']
         end
     
         def map_accel_path(env, path)
           if mapping = @mappings.find { |internal, _| internal =~ path }
    -        path.sub(*mapping)
    -      elsif mapping = env['HTTP_X_ACCEL_MAPPING']
    +        return path.sub(*mapping)
    +      elsif mapping = x_accel_mapping(env)
    +        # Safe to use header: explicit config + no app mappings:
             mapping.split(',').map(&:strip).each do |m|
               internal, external = m.split('=', 2).map(&:strip)
    -          new_path = path.sub(/^#{internal}/i, external)
    +          new_path = path.sub(/\A#{internal}/i, external)
               return new_path unless path == new_path
             end
    -        path
    +
    +        return path
           end
         end
       end
    
  • test/spec_sendfile.rb+137 30 modified
    @@ -22,12 +22,12 @@ def simple_app(body = sendfile_body)
         lambda { |env| [200, { 'content-type' => 'text/plain' }, body] }
       end
     
    -  def sendfile_app(body, mappings = [])
    -    Rack::Lint.new Rack::Sendfile.new(simple_app(body), nil, mappings)
    +  def sendfile_app(body, mappings = [], variation = nil)
    +    Rack::Lint.new Rack::Sendfile.new(simple_app(body), variation, mappings)
       end
     
    -  def request(headers = {}, body = sendfile_body, mappings = [])
    -    yield Rack::MockRequest.new(sendfile_app(body, mappings)).get('/', headers)
    +  def request(headers = {}, body = sendfile_body, mappings = [], variation = nil)
    +    yield Rack::MockRequest.new(sendfile_app(body, mappings, variation)).get('/', headers)
       end
     
       def open_file(path)
    @@ -48,7 +48,8 @@ def open_file(path)
     
       it "does nothing and logs to rack.errors when incorrect x-sendfile-type header present" do
         io = StringIO.new
    -    request 'HTTP_X_SENDFILE_TYPE' => 'X-Banana', 'rack.errors' => io do |response|
    +    # Configure with wrong variation type
    +    request({ 'rack.errors' => io }, sendfile_body, [], 'X-Banana') do |response|
           response.must_be :ok?
           response.body.must_equal 'Hello World'
           response.headers.wont_include 'x-sendfile'
    @@ -59,7 +60,7 @@ def open_file(path)
       end
     
       it "sets x-sendfile response header and discards body" do
    -    request 'HTTP_X_SENDFILE_TYPE' => 'x-sendfile' do |response|
    +    request({}, sendfile_body, [], 'X-Sendfile') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['content-length'].must_equal '0'
    @@ -71,7 +72,7 @@ def open_file(path)
         body = sendfile_body
         closed = false
         body.define_singleton_method(:close){closed = true}
    -    request({'HTTP_X_SENDFILE_TYPE' => 'x-sendfile'}, body) do |response|
    +    request({}, body, [], 'X-Sendfile') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['content-length'].must_equal '0'
    @@ -81,7 +82,7 @@ def open_file(path)
       end
     
       it "sets x-lighttpd-send-file response header and discards body" do
    -    request 'HTTP_X_SENDFILE_TYPE' => 'x-lighttpd-send-file' do |response|
    +    request({}, sendfile_body, [], 'X-Lighttpd-Send-File') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['content-length'].must_equal '0'
    @@ -91,10 +92,9 @@ def open_file(path)
     
       it "sets x-accel-redirect response header and discards body" do
         headers = {
    -      'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect',
           'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar/"
         }
    -    request headers do |response|
    +    request(headers, sendfile_body, [], 'X-Accel-Redirect') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['content-length'].must_equal '0'
    @@ -104,10 +104,9 @@ def open_file(path)
     
       it "sets x-accel-redirect response header to percent-encoded path" do
         headers = {
    -      'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect',
           'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar%/"
         }
    -    request headers, sendfile_body('file_with_%_?_symbol') do |response|
    +    request(headers, sendfile_body('file_with_%_?_symbol'), [], 'X-Accel-Redirect') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['content-length'].must_equal '0'
    @@ -116,7 +115,7 @@ def open_file(path)
       end
     
       it 'writes to rack.error when no x-accel-mapping is specified' do
    -    request 'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect' do |response|
    +    request({}, sendfile_body, [], 'X-Accel-Redirect') do |response|
           response.must_be :ok?
           response.body.must_equal 'Hello World'
           response.headers.wont_include 'x-accel-redirect'
    @@ -125,7 +124,7 @@ def open_file(path)
       end
     
       it 'does nothing when body does not respond to #to_path' do
    -    request({ 'HTTP_X_SENDFILE_TYPE' => 'x-sendfile' }, ['Not a file...']) do |response|
    +    request({}, ['Not a file...'], [], 'X-Sendfile') do |response|
           response.body.must_equal 'Not a file...'
           response.headers.wont_include 'x-sendfile'
         end
    @@ -147,14 +146,14 @@ def open_file(path)
             ["#{dir2}/", '/wibble/']
           ]
     
    -      request({ 'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect' }, first_body, mappings) do |response|
    +      request({}, first_body, mappings, 'X-Accel-Redirect') do |response|
             response.must_be :ok?
             response.body.must_be :empty?
             response.headers['content-length'].must_equal '0'
             response.headers['x-accel-redirect'].must_equal '/foo/bar/rack_sendfile'
           end
     
    -      request({ 'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect' }, second_body, mappings) do |response|
    +      request({}, second_body, mappings, 'X-Accel-Redirect') do |response|
             response.must_be :ok?
             response.body.must_be :empty?
             response.headers['content-length'].must_equal '0'
    @@ -181,34 +180,142 @@ def open_file(path)
           third_body = open_file(File.join(dir3, 'rack_sendfile'))
           third_body.puts 'hello again world'
     
    +      # Now we need to explicitly enable x-accel-redirect in the constructor
    +      app = Rack::Lint.new Rack::Sendfile.new(simple_app(first_body), "X-Accel-Redirect", [])
    +      
           headers = {
    -        'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect',
             'HTTP_X_ACCEL_MAPPING' => "#{dir1}/=/foo/bar/, #{dir2}/=/wibble/"
           }
     
    -      request(headers, first_body) do |response|
    +      response = Rack::MockRequest.new(app).get('/', headers)
    +      response.must_be :ok?
    +      response.body.must_be :empty?
    +      response.headers['content-length'].must_equal '0'
    +      response.headers['x-accel-redirect'].must_equal '/foo/bar/rack_sendfile'
    +
    +      app = Rack::Lint.new Rack::Sendfile.new(simple_app(second_body), "X-Accel-Redirect", [])
    +      response = Rack::MockRequest.new(app).get('/', headers)
    +      response.must_be :ok?
    +      response.body.must_be :empty?
    +      response.headers['content-length'].must_equal '0'
    +      response.headers['x-accel-redirect'].must_equal '/wibble/rack_sendfile'
    +
    +      app = Rack::Lint.new Rack::Sendfile.new(simple_app(third_body), "X-Accel-Redirect", [])
    +      response = Rack::MockRequest.new(app).get('/', headers)
    +      response.must_be :ok?
    +      response.body.must_be :empty?
    +      response.headers['content-length'].must_equal '0'
    +      response.headers['x-accel-redirect'].must_equal "#{dir3}/rack_sendfile"
    +    ensure
    +      FileUtils.remove_entry_secure dir1
    +      FileUtils.remove_entry_secure dir2
    +      FileUtils.remove_entry_secure dir3
    +    end
    +  end
    +
    +  # Security tests for CVE mitigation
    +  describe "security: information disclosure prevention" do
    +    it "ignores HTTP_X_SENDFILE_TYPE header to prevent attacker-controlled sendfile activation" do
    +      # Attacker tries to enable x-sendfile via header
    +      request 'HTTP_X_SENDFILE_TYPE' => 'x-sendfile' do |response|
             response.must_be :ok?
    -        response.body.must_be :empty?
    -        response.headers['content-length'].must_equal '0'
    -        response.headers['x-accel-redirect'].must_equal '/foo/bar/rack_sendfile'
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-sendfile'
           end
    +    end
     
    -      request(headers, second_body) do |response|
    +    it "ignores HTTP_X_SENDFILE_TYPE header attempting to enable x-accel-redirect" do
    +      # Attacker tries to enable x-accel-redirect via header with mapping
    +      headers = {
    +        'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect',
    +        'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/attacker/path/"
    +      }
    +      request headers do |response|
    +        response.must_be :ok?
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-accel-redirect'
    +      end
    +    end
    +
    +    it "ignores HTTP_X_ACCEL_MAPPING when x-accel-redirect is not explicitly enabled" do
    +      # Even if attacker sends mapping header, it should be ignored without explicit config
    +      headers = {
    +        'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/attacker/path/"
    +      }
    +      request headers do |response|
    +        response.must_be :ok?
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-accel-redirect'
    +      end
    +    end
    +
    +    it "ignores HTTP_X_ACCEL_MAPPING when application-level mappings are configured" do
    +      # When app provides mappings, header should be ignored for security
    +      begin
    +        dir = Dir.mktmpdir
    +        body = open_file(File.join(dir, 'rack_sendfile'))
    +        body.puts 'test'
    +        
    +        app_mappings = [["#{dir}/", '/app/mapping/']]
    +        app = Rack::Lint.new Rack::Sendfile.new(simple_app(body), "X-Accel-Redirect", app_mappings)
    +        
    +        headers = {
    +          'HTTP_X_ACCEL_MAPPING' => "#{dir}/=/attacker/path/"
    +        }
    +        
    +        response = Rack::MockRequest.new(app).get('/', headers)
             response.must_be :ok?
             response.body.must_be :empty?
    -        response.headers['content-length'].must_equal '0'
    -        response.headers['x-accel-redirect'].must_equal '/wibble/rack_sendfile'
    +        response.headers['x-accel-redirect'].must_equal '/app/mapping/rack_sendfile'
    +        response.headers['x-accel-redirect'].wont_equal '/attacker/path/rack_sendfile'
    +      ensure
    +        FileUtils.remove_entry_secure dir
           end
    +    end
     
    -      request(headers, third_body) do |response|
    +    it "allows HTTP_X_ACCEL_MAPPING only when x-accel-redirect explicitly enabled with no app mappings" do
    +      # This is the safe use case: explicit config + no app mappings = allow header
    +      begin
    +        dir = Dir.mktmpdir
    +        body = open_file(File.join(dir, 'rack_sendfile'))
    +        body.puts 'test'
    +        
    +        app = Rack::Lint.new Rack::Sendfile.new(simple_app(body), "X-Accel-Redirect", [])
    +        
    +        headers = {
    +          'HTTP_X_ACCEL_MAPPING' => "#{dir}/=/safe/nginx/mapping/"
    +        }
    +        
    +        response = Rack::MockRequest.new(app).get('/', headers)
             response.must_be :ok?
             response.body.must_be :empty?
    -        response.headers['content-length'].must_equal '0'
    -        response.headers['x-accel-redirect'].must_equal "#{dir3}/rack_sendfile"
    +        response.headers['x-accel-redirect'].must_equal '/safe/nginx/mapping/rack_sendfile'
    +      ensure
    +        FileUtils.remove_entry_secure dir
           end
    -    ensure
    -      FileUtils.remove_entry_secure dir1
    -      FileUtils.remove_entry_secure dir2
    +    end
    +
    +    it "does not allow x-lighttpd-send-file activation via header" do
    +      # Verify other sendfile types also can't be enabled via headers
    +      request 'HTTP_X_SENDFILE_TYPE' => 'x-lighttpd-send-file' do |response|
    +        response.must_be :ok?
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-lighttpd-send-file'
    +      end
    +    end
    +
    +    it "requires explicit middleware configuration for any sendfile variation" do
    +      # Test that sendfile.type env var still works (internal, not from HTTP headers)
    +      body = sendfile_body
    +      app = lambda { |env| [200, { 'content-type' => 'text/plain' }, body] }
    +      middleware = Rack::Lint.new Rack::Sendfile.new(app)
    +      
    +      env = Rack::MockRequest.env_for('/', { 'sendfile.type' => 'x-sendfile' })
    +      status, headers, response_body = middleware.call(env)
    +      
    +      status.must_equal 200
    +      headers['x-sendfile'].must_equal File.join(Dir.tmpdir, "rack_sendfile")
    +      headers['content-length'].must_equal '0'
         end
       end
     end
    
fba2c8bc63eb

Improper handling of proxy headers in `Rack::Sendfile` may allow proxy bypass.

https://github.com/rack/rackSamuel WilliamsOct 7, 2025via ghsa
3 files changed · +200 44
  • CHANGELOG.md+6 0 modified
    @@ -2,6 +2,12 @@
     
     All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/).
     
    +## [2.2.20] - 2025-10-10
    +
    +### Security
    +
    +- [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
    +
     ## [2.2.19] - 2025-10-07
     
     ### Security
    
  • lib/rack/sendfile.rb+40 10 modified
    @@ -40,18 +40,23 @@ module Rack
       #     proxy_set_header   X-Real-IP           $remote_addr;
       #     proxy_set_header   X-Forwarded-For     $proxy_add_x_forwarded_for;
       #
    -  #     proxy_set_header   X-Sendfile-Type     X-Accel-Redirect;
       #     proxy_set_header   X-Accel-Mapping     /var/www/=/files/;
       #
       #     proxy_pass         http://127.0.0.1:8080/;
       #   }
       #
    -  # Note that the X-Sendfile-Type header must be set exactly as shown above.
       # The X-Accel-Mapping header should specify the location on the file system,
       # followed by an equals sign (=), followed name of the private URL pattern
       # that it maps to. The middleware performs a simple substitution on the
       # resulting path.
       #
    +  # To enable X-Accel-Redirect, you must configure the middleware explicitly:
    +  #
    +  #   use Rack::Sendfile, "X-Accel-Redirect"
    +  #
    +  # For security reasons, the X-Sendfile-Type header from requests is ignored.
    +  # The sendfile variation must be set via the middleware constructor.
    +  #
       # See Also: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile
       #
       # === lighttpd
    @@ -96,13 +101,25 @@ module Rack
       # X-Accel-Mapping header. Mappings should be provided in tuples of internal to
       # external. The internal values may contain regular expression syntax, they
       # will be matched with case indifference.
    +  #
    +  # When X-Accel-Redirect is explicitly enabled via the variation parameter,
    +  # and no application-level mappings are provided, the middleware will read
    +  # the X-Accel-Mapping header from the proxy. This allows nginx to control
    +  # the path mapping without requiring application-level configuration.
    +  #
    +  # === Security
    +  #
    +  # For security reasons, the X-Sendfile-Type header from HTTP requests is
    +  # ignored. The sendfile variation must be explicitly configured via the
    +  # middleware constructor to prevent information disclosure vulnerabilities
    +  # where attackers could bypass proxy restrictions.
     
       class Sendfile
         def initialize(app, variation = nil, mappings = [])
           @app = app
           @variation = variation
           @mappings = mappings.map do |internal, external|
    -        [/^#{internal}/i, external]
    +        [/\A#{internal}/i, external]
           end
         end
     
    @@ -140,22 +157,35 @@ def call(env)
         end
     
         private
    +
         def variation(env)
    -      @variation ||
    -        env['sendfile.type'] ||
    -        env['HTTP_X_SENDFILE_TYPE']
    +      # Note: HTTP_X_SENDFILE_TYPE is intentionally NOT read for security reasons.
    +      # Attackers could use this header to enable x-accel-redirect and bypass proxy restrictions.
    +      @variation || env['sendfile.type']
    +    end
    +
    +    def x_accel_mapping(env)
    +      # Only allow header when:
    +      # 1. X-Accel-Redirect is explicitly enabled via constructor.
    +      # 2. No application-level mappings are configured.
    +      return nil unless @variation =~ /x-accel-redirect/i
    +      return nil if @mappings.any?
    +      
    +      env['HTTP_X_ACCEL_MAPPING']
         end
     
         def map_accel_path(env, path)
           if mapping = @mappings.find { |internal, _| internal =~ path }
    -        path.sub(*mapping)
    -      elsif mapping = env['HTTP_X_ACCEL_MAPPING']
    +        return path.sub(*mapping)
    +      elsif mapping = x_accel_mapping(env)
    +        # Safe to use header: explicit config + no app mappings:
             mapping.split(',').map(&:strip).each do |m|
               internal, external = m.split('=', 2).map(&:strip)
    -          new_path = path.sub(/^#{internal}/i, external)
    +          new_path = path.sub(/\A#{internal}/i, external)
               return new_path unless path == new_path
             end
    -        path
    +
    +        return path
           end
         end
       end
    
  • test/spec_sendfile.rb+154 34 modified
    @@ -16,12 +16,12 @@ def simple_app(body = sendfile_body)
         lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] }
       end
     
    -  def sendfile_app(body, mappings = [])
    -    Rack::Lint.new Rack::Sendfile.new(simple_app(body), nil, mappings)
    +  def sendfile_app(body, mappings = [], variation = nil)
    +    Rack::Lint.new Rack::Sendfile.new(simple_app(body), variation, mappings)
       end
     
    -  def request(headers = {}, body = sendfile_body, mappings = [])
    -    yield Rack::MockRequest.new(sendfile_app(body, mappings)).get('/', headers)
    +  def request(headers = {}, body = sendfile_body, mappings = [], variation = nil)
    +    yield Rack::MockRequest.new(sendfile_app(body, mappings, variation)).get('/', headers)
       end
     
       def open_file(path)
    @@ -42,7 +42,8 @@ def open_file(path)
     
       it "does nothing and logs to rack.errors when incorrect X-Sendfile-Type header present" do
         io = StringIO.new
    -    request 'HTTP_X_SENDFILE_TYPE' => 'X-Banana', 'rack.errors' => io do |response|
    +    # Configure with wrong variation type
    +    request({ 'rack.errors' => io }, sendfile_body, [], 'X-Banana') do |response|
           response.must_be :ok?
           response.body.must_equal 'Hello World'
           response.headers.wont_include 'X-Sendfile'
    @@ -52,30 +53,42 @@ def open_file(path)
         end
       end
     
    -  it "sets X-Sendfile response header and discards body" do
    -    request 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' do |response|
    +  it "sets x-sendfile response header and discards body" do
    +    request({}, sendfile_body, [], 'X-Sendfile') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['Content-Length'].must_equal '0'
           response.headers['X-Sendfile'].must_equal File.join(Dir.tmpdir,  "rack_sendfile")
         end
       end
     
    -  it "sets X-Lighttpd-Send-File response header and discards body" do
    -    request 'HTTP_X_SENDFILE_TYPE' => 'X-Lighttpd-Send-File' do |response|
    +  it "closes body when x-sendfile used" do
    +    body = sendfile_body
    +    closed = false
    +    body.define_singleton_method(:close){closed = true}
    +    request({}, body, [], 'X-Sendfile') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
    -      response.headers['Content-Length'].must_equal '0'
    -      response.headers['X-Lighttpd-Send-File'].must_equal File.join(Dir.tmpdir,  "rack_sendfile")
    +      response.headers['content-length'].must_equal '0'
    +      response.headers['x-sendfile'].must_equal File.join(Dir.tmpdir,  "rack_sendfile")
    +    end
    +    closed.must_equal true
    +  end
    +
    +  it "sets x-lighttpd-send-file response header and discards body" do
    +    request({}, sendfile_body, [], 'X-Lighttpd-Send-File') do |response|
    +      response.must_be :ok?
    +      response.body.must_be :empty?
    +      response.headers['content-length'].must_equal '0'
    +      response.headers['x-lighttpd-send-file'].must_equal File.join(Dir.tmpdir,  "rack_sendfile")
         end
       end
     
       it "sets X-Accel-Redirect response header and discards body" do
         headers = {
    -      'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
           'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar/"
         }
    -    request headers do |response|
    +    request(headers, sendfile_body, [], 'X-Accel-Redirect') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['Content-Length'].must_equal '0'
    @@ -85,19 +98,18 @@ def open_file(path)
     
       it "sets X-Accel-Redirect response header to percent-encoded path" do
         headers = {
    -      'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
           'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar%/"
         }
    -    request headers, sendfile_body('file_with_%_?_symbol') do |response|
    +    request(headers, sendfile_body('file_with_%_?_symbol'), [], 'X-Accel-Redirect') do |response|
           response.must_be :ok?
           response.body.must_be :empty?
           response.headers['Content-Length'].must_equal '0'
           response.headers['X-Accel-Redirect'].must_equal '/foo/bar%25/file_with_%25_%3F_symbol'
         end
       end
     
    -  it 'writes to rack.error when no X-Accel-Mapping is specified' do
    -    request 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' do |response|
    +  it 'writes to rack.error when no x-accel-mapping is specified' do
    +    request({}, sendfile_body, [], 'X-Accel-Redirect') do |response|
           response.must_be :ok?
           response.body.must_equal 'Hello World'
           response.headers.wont_include 'X-Accel-Redirect'
    @@ -106,7 +118,7 @@ def open_file(path)
       end
     
       it 'does nothing when body does not respond to #to_path' do
    -    request({ 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' }, ['Not a file...']) do |response|
    +    request({}, ['Not a file...'], [], 'X-Sendfile') do |response|
           response.body.must_equal 'Not a file...'
           response.headers.wont_include 'X-Sendfile'
         end
    @@ -128,14 +140,14 @@ def open_file(path)
             ["#{dir2}/", '/wibble/']
           ]
     
    -      request({ 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' }, first_body, mappings) do |response|
    +      request({}, first_body, mappings, 'X-Accel-Redirect') do |response|
             response.must_be :ok?
             response.body.must_be :empty?
             response.headers['Content-Length'].must_equal '0'
             response.headers['X-Accel-Redirect'].must_equal '/foo/bar/rack_sendfile'
           end
     
    -      request({ 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' }, second_body, mappings) do |response|
    +      request({}, second_body, mappings, 'X-Accel-Redirect') do |response|
             response.must_be :ok?
             response.body.must_be :empty?
             response.headers['Content-Length'].must_equal '0'
    @@ -162,34 +174,142 @@ def open_file(path)
           third_body = open_file(File.join(dir3, 'rack_sendfile'))
           third_body.puts 'hello again world'
     
    +      # Now we need to explicitly enable x-accel-redirect in the constructor
    +      app = Rack::Lint.new Rack::Sendfile.new(simple_app(first_body), "X-Accel-Redirect", [])
    +      
           headers = {
    -        'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
             'HTTP_X_ACCEL_MAPPING' => "#{dir1}/=/foo/bar/, #{dir2}/=/wibble/"
           }
     
    -      request(headers, first_body) do |response|
    +      response = Rack::MockRequest.new(app).get('/', headers)
    +      response.must_be :ok?
    +      response.body.must_be :empty?
    +      response.headers['content-length'].must_equal '0'
    +      response.headers['x-accel-redirect'].must_equal '/foo/bar/rack_sendfile'
    +
    +      app = Rack::Lint.new Rack::Sendfile.new(simple_app(second_body), "X-Accel-Redirect", [])
    +      response = Rack::MockRequest.new(app).get('/', headers)
    +      response.must_be :ok?
    +      response.body.must_be :empty?
    +      response.headers['content-length'].must_equal '0'
    +      response.headers['x-accel-redirect'].must_equal '/wibble/rack_sendfile'
    +
    +      app = Rack::Lint.new Rack::Sendfile.new(simple_app(third_body), "X-Accel-Redirect", [])
    +      response = Rack::MockRequest.new(app).get('/', headers)
    +      response.must_be :ok?
    +      response.body.must_be :empty?
    +      response.headers['content-length'].must_equal '0'
    +      response.headers['x-accel-redirect'].must_equal "#{dir3}/rack_sendfile"
    +    ensure
    +      FileUtils.remove_entry_secure dir1
    +      FileUtils.remove_entry_secure dir2
    +      FileUtils.remove_entry_secure dir3
    +    end
    +  end
    +
    +  # Security tests for CVE mitigation
    +  describe "security: information disclosure prevention" do
    +    it "ignores HTTP_X_SENDFILE_TYPE header to prevent attacker-controlled sendfile activation" do
    +      # Attacker tries to enable x-sendfile via header
    +      request 'HTTP_X_SENDFILE_TYPE' => 'x-sendfile' do |response|
             response.must_be :ok?
    -        response.body.must_be :empty?
    -        response.headers['Content-Length'].must_equal '0'
    -        response.headers['X-Accel-Redirect'].must_equal '/foo/bar/rack_sendfile'
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-sendfile'
           end
    +    end
     
    -      request(headers, second_body) do |response|
    +    it "ignores HTTP_X_SENDFILE_TYPE header attempting to enable x-accel-redirect" do
    +      # Attacker tries to enable x-accel-redirect via header with mapping
    +      headers = {
    +        'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect',
    +        'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/attacker/path/"
    +      }
    +      request headers do |response|
    +        response.must_be :ok?
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-accel-redirect'
    +      end
    +    end
    +
    +    it "ignores HTTP_X_ACCEL_MAPPING when x-accel-redirect is not explicitly enabled" do
    +      # Even if attacker sends mapping header, it should be ignored without explicit config
    +      headers = {
    +        'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/attacker/path/"
    +      }
    +      request headers do |response|
    +        response.must_be :ok?
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-accel-redirect'
    +      end
    +    end
    +
    +    it "ignores HTTP_X_ACCEL_MAPPING when application-level mappings are configured" do
    +      # When app provides mappings, header should be ignored for security
    +      begin
    +        dir = Dir.mktmpdir
    +        body = open_file(File.join(dir, 'rack_sendfile'))
    +        body.puts 'test'
    +        
    +        app_mappings = [["#{dir}/", '/app/mapping/']]
    +        app = Rack::Lint.new Rack::Sendfile.new(simple_app(body), "X-Accel-Redirect", app_mappings)
    +        
    +        headers = {
    +          'HTTP_X_ACCEL_MAPPING' => "#{dir}/=/attacker/path/"
    +        }
    +        
    +        response = Rack::MockRequest.new(app).get('/', headers)
             response.must_be :ok?
             response.body.must_be :empty?
    -        response.headers['Content-Length'].must_equal '0'
    -        response.headers['X-Accel-Redirect'].must_equal '/wibble/rack_sendfile'
    +        response.headers['x-accel-redirect'].must_equal '/app/mapping/rack_sendfile'
    +        response.headers['x-accel-redirect'].wont_equal '/attacker/path/rack_sendfile'
    +      ensure
    +        FileUtils.remove_entry_secure dir
           end
    +    end
     
    -      request(headers, third_body) do |response|
    +    it "allows HTTP_X_ACCEL_MAPPING only when x-accel-redirect explicitly enabled with no app mappings" do
    +      # This is the safe use case: explicit config + no app mappings = allow header
    +      begin
    +        dir = Dir.mktmpdir
    +        body = open_file(File.join(dir, 'rack_sendfile'))
    +        body.puts 'test'
    +        
    +        app = Rack::Lint.new Rack::Sendfile.new(simple_app(body), "X-Accel-Redirect", [])
    +        
    +        headers = {
    +          'HTTP_X_ACCEL_MAPPING' => "#{dir}/=/safe/nginx/mapping/"
    +        }
    +        
    +        response = Rack::MockRequest.new(app).get('/', headers)
             response.must_be :ok?
             response.body.must_be :empty?
    -        response.headers['Content-Length'].must_equal '0'
    -        response.headers['X-Accel-Redirect'].must_equal "#{dir3}/rack_sendfile"
    +        response.headers['x-accel-redirect'].must_equal '/safe/nginx/mapping/rack_sendfile'
    +      ensure
    +        FileUtils.remove_entry_secure dir
           end
    -    ensure
    -      FileUtils.remove_entry_secure dir1
    -      FileUtils.remove_entry_secure dir2
    +    end
    +
    +    it "does not allow x-lighttpd-send-file activation via header" do
    +      # Verify other sendfile types also can't be enabled via headers
    +      request 'HTTP_X_SENDFILE_TYPE' => 'x-lighttpd-send-file' do |response|
    +        response.must_be :ok?
    +        response.body.must_equal 'Hello World'
    +        response.headers.wont_include 'x-lighttpd-send-file'
    +      end
    +    end
    +
    +    it "requires explicit middleware configuration for any sendfile variation" do
    +      # Test that sendfile.type env var still works (internal, not from HTTP headers)
    +      body = sendfile_body
    +      app = lambda { |env| [200, { 'content-type' => 'text/plain' }, body] }
    +      middleware = Rack::Lint.new Rack::Sendfile.new(app)
    +      
    +      env = Rack::MockRequest.env_for('/', { 'sendfile.type' => 'X-Sendfile' })
    +      status, headers, response_body = middleware.call(env)
    +      
    +      status.must_equal 200
    +      headers['X-Sendfile'].must_equal File.join(Dir.tmpdir, "rack_sendfile")
    +      headers['Content-Length'].must_equal '0'
         end
       end
     end
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.