Skip to content
Draft
Next Next commit
fix(cookies): fix compatibility with rack 3
Do not join cookies with new like if they weren't before

fix(middleware): ensure headers are wrapped with `Rack::Headers`

Add `Rack::Headers` wrapping to middleware to
prevent header manipulation issues. Added a test
to verify cookies remain as an array when flagged
if already in array format.
  • Loading branch information
deril authored and denn-os committed Dec 16, 2025
commit d13304415c5b0b8f721a60a3492abeed41489350
13 changes: 6 additions & 7 deletions lib/secure_headers/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def initialize(app)
def call(env)
req = Rack::Request.new(env)
status, headers, response = @app.call(env)
headers = Rack::Headers[headers]

config = SecureHeaders.config_for(req)
flag_cookies!(headers, override_secure(env, config.cookies)) unless config.cookies == OPT_OUT
Expand All @@ -20,14 +21,12 @@ def call(env)

# inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
def flag_cookies!(headers, config)
if cookies = headers["Set-Cookie"]
# Support Rails 2.3 / Rack 1.1 arrays as headers
cookies = cookies.split("\n") unless cookies.is_a?(Array)
cookies = headers["Set-Cookie"]
return unless cookies

headers["Set-Cookie"] = cookies.map do |cookie|
SecureHeaders::Cookie.new(cookie, config).to_s
end.join("\n")
end
cookies_array = cookies.is_a?(Array) ? cookies : cookies.split("\n")
secured_cookies = cookies_array.map { |cookie| SecureHeaders::Cookie.new(cookie, config).to_s }
headers["Set-Cookie"] = cookies.is_a?(Array) ? secured_cookies : secured_cookies.join("\n")
end

# disable Secure cookies for non-https requests
Expand Down
14 changes: 12 additions & 2 deletions spec/lib/secure_headers/middleware_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,26 @@ module SecureHeaders
end

it "flags cookies with a combination of SameSite configurations" do
cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] })
cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => "_session=foobar\n_guest=true"), "app"] })

Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: OPT_OUT, secure: OPT_OUT} }
Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: OPT_OUT, secure: OPT_OUT } }
request = Rack::Request.new("HTTPS" => "on")
_, env = cookie_middleware.call request.env

expect(env["Set-Cookie"]).to match("_session=foobar; SameSite=Strict")
expect(env["Set-Cookie"]).to match("_guest=true; SameSite=Lax")
end

it "keeps cookies as array after flagging if they are already an array" do
cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] })

Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: OPT_OUT, secure: OPT_OUT } }
request = Rack::Request.new("HTTPS" => "on")
_, env = cookie_middleware.call request.env

expect(env["Set-Cookie"]).to match_array(["_session=foobar; SameSite=Strict", "_guest=true; SameSite=Lax"])
end

it "disables secure cookies for non-https requests" do
Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT, samesite: OPT_OUT } }

Expand Down