Commit 2adfb1a1 authored by Stan Hu's avatar Stan Hu

Reintroduce Rack v2.1.4

This is needed to address a few outstanding CVEs and fix cookie
timestamp formats.

Full list of changes:
https://github.com/rack/rack/compare/2.0.9..2.1.4

Relates to:
* https://gitlab.com/gitlab-org/gitlab/-/issues/36362
* https://gitlab.com/gitlab-org/gitlab/-/issues/228622

Rack v2.1.x no longer coerces the body to a string. The Rack spec
(https://github.com/rack/rack/blob/master/SPEC.rdoc#the-body-) says:

The Body must respond to `each` and must only yield String values

Previously in a few places the Grape API was returning the status code
as an integer, which Grape used as the response body. To preserve the
legacy behavior, we explicitly set the body to the stringified integer.

In https://gitlab.com/gitlab-org/gitlab/-/issues/267598, we saw Maven
packages report 500 errors because a `nil` body was being returned. This
has been fixed in
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45271.

Because it's easy to mistakenly return the wrong type in the Grape body,
this commit also adds a new Grape middleware that will automatically
coerce values to strings but raise an exception in development and test.

This reverts https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45246
and brings back
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44518.
parent d9cba039
...@@ -172,7 +172,7 @@ gem 'diffy', '~> 3.3' ...@@ -172,7 +172,7 @@ gem 'diffy', '~> 3.3'
gem 'diff_match_patch', '~> 0.1.0' gem 'diff_match_patch', '~> 0.1.0'
# Application server # Application server
gem 'rack', '~> 2.0.9' gem 'rack', '~> 2.1.4'
# https://github.com/sharpstone/rack-timeout/blob/master/README.md#rails-apps-manually # https://github.com/sharpstone/rack-timeout/blob/master/README.md#rails-apps-manually
gem 'rack-timeout', '~> 0.5.1', require: 'rack/timeout/base' gem 'rack-timeout', '~> 0.5.1', require: 'rack/timeout/base'
......
...@@ -855,7 +855,7 @@ GEM ...@@ -855,7 +855,7 @@ GEM
public_suffix (4.0.6) public_suffix (4.0.6)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
raabro (1.1.6) raabro (1.1.6)
rack (2.0.9) rack (2.1.4)
rack-accept (0.4.5) rack-accept (0.4.5)
rack (>= 0.4) rack (>= 0.4)
rack-attack (6.3.0) rack-attack (6.3.0)
...@@ -1429,7 +1429,7 @@ DEPENDENCIES ...@@ -1429,7 +1429,7 @@ DEPENDENCIES
prometheus-client-mmap (~> 0.12.0) prometheus-client-mmap (~> 0.12.0)
pry-byebug (~> 3.9.0) pry-byebug (~> 3.9.0)
pry-rails (~> 0.3.9) pry-rails (~> 0.3.9)
rack (~> 2.0.9) rack (~> 2.1.4)
rack-attack (~> 6.3.0) rack-attack (~> 6.3.0)
rack-cors (~> 1.0.6) rack-cors (~> 1.0.6)
rack-oauth2 (~> 1.9.3) rack-oauth2 (~> 1.9.3)
......
...@@ -183,7 +183,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -183,7 +183,7 @@ class Admin::UsersController < Admin::ApplicationController
# restore username to keep form action url. # restore username to keep form action url.
user.username = params[:id] user.username = params[:id]
format.html { render "edit" } format.html { render "edit" }
format.json { render json: [result[:message]], status: result[:status] } format.json { render json: [result[:message]], status: :internal_server_error }
end end
end end
end end
......
...@@ -45,7 +45,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli ...@@ -45,7 +45,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
if result[:status] == :success if result[:status] == :success
head :ok head :ok
else else
render json: { message: result[:message] }, status: result[:status] render json: { message: result[:message] }, status: :internal_server_error
end end
end end
......
---
title: Update to Rack v2.1.4
merge_request: 45340
author:
type: fixed
...@@ -19,6 +19,7 @@ module API ...@@ -19,6 +19,7 @@ module API
end end
use AdminModeMiddleware use AdminModeMiddleware
use ResponseCoercerMiddleware
helpers HelperMethods helpers HelperMethods
...@@ -188,6 +189,44 @@ module API ...@@ -188,6 +189,44 @@ module API
end end
end end
# Prior to Rack v2.1.x, returning a body of [nil] or [201] worked
# because the body was coerced to a string. However, this no longer
# works in Rack v2.1.0+. The Rack spec
# (https://github.com/rack/rack/blob/master/SPEC.rdoc#the-body-)
# says:
#
# The Body must respond to `each` and must only yield String values
#
# Because it's easy to return the wrong body type, this middleware
# will:
#
# 1. Inspect each element of the body if it is an Array.
# 2. Coerce each value to a string if necessary.
# 3. Flag a test and development error.
class ResponseCoercerMiddleware < ::Grape::Middleware::Base
def call(env)
response = super(env)
status = response[0]
body = response[2]
return response if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY[status]
return response unless body.is_a?(Array)
body.map! do |part|
if part.is_a?(String)
part
else
err = ArgumentError.new("The response body should be a String, but it is of type #{part.class}")
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(err)
part.to_s
end
end
response
end
end
class AdminModeMiddleware < ::Grape::Middleware::Base class AdminModeMiddleware < ::Grape::Middleware::Base
def after def after
# Use a Grape middleware since the Grape `after` blocks might run # Use a Grape middleware since the Grape `after` blocks might run
......
...@@ -72,6 +72,7 @@ module API ...@@ -72,6 +72,7 @@ module API
post '/verify' do post '/verify' do
authenticate_runner! authenticate_runner!
status 200 status 200
body "200"
end end
end end
...@@ -183,6 +184,7 @@ module API ...@@ -183,6 +184,7 @@ module API
service.execute.then do |result| service.execute.then do |result|
header 'X-GitLab-Trace-Update-Interval', result.backoff header 'X-GitLab-Trace-Update-Interval', result.backoff
status result.status status result.status
body result.status.to_s
end end
end end
...@@ -293,6 +295,7 @@ module API ...@@ -293,6 +295,7 @@ module API
if result[:status] == :success if result[:status] == :success
status :created status :created
body "201"
else else
render_api_error!(result[:message], result[:http_status]) render_api_error!(result[:message], result[:http_status])
end end
......
...@@ -522,7 +522,7 @@ module API ...@@ -522,7 +522,7 @@ module API
else else
header(*Gitlab::Workhorse.send_url(file.url)) header(*Gitlab::Workhorse.send_url(file.url))
status :ok status :ok
body body ""
end end
end end
......
...@@ -44,7 +44,7 @@ module API ...@@ -44,7 +44,7 @@ module API
workhorse_headers = Gitlab::Workhorse.send_url(file.url) workhorse_headers = Gitlab::Workhorse.send_url(file.url)
header workhorse_headers[0], workhorse_headers[1] header workhorse_headers[0], workhorse_headers[1]
env['api.format'] = :binary env['api.format'] = :binary
body nil body ""
end end
end end
end end
......
...@@ -390,7 +390,7 @@ RSpec.describe Admin::UsersController do ...@@ -390,7 +390,7 @@ RSpec.describe Admin::UsersController do
describe 'POST update' do describe 'POST update' do
context 'when the password has changed' do context 'when the password has changed' do
def update_password(user, password = User.random_password, password_confirmation = password) def update_password(user, password = User.random_password, password_confirmation = password, format = :html)
params = { params = {
id: user.to_param, id: user.to_param,
user: { user: {
...@@ -399,7 +399,7 @@ RSpec.describe Admin::UsersController do ...@@ -399,7 +399,7 @@ RSpec.describe Admin::UsersController do
} }
} }
post :update, params: params post :update, params: params, format: format
end end
context 'when admin changes their own password' do context 'when admin changes their own password' do
...@@ -498,6 +498,23 @@ RSpec.describe Admin::UsersController do ...@@ -498,6 +498,23 @@ RSpec.describe Admin::UsersController do
.not_to change { user.reload.encrypted_password } .not_to change { user.reload.encrypted_password }
end end
end end
context 'when the update fails' do
let(:password) { User.random_password }
before do
expect_next_instance_of(Users::UpdateService) do |service|
allow(service).to receive(:execute).and_return({ message: 'failed', status: :error })
end
end
it 'returns a 500 error' do
expect { update_password(admin, password, password, :json) }
.not_to change { admin.reload.password_expired? }
expect(response).to have_gitlab_http_status(:error)
end
end
end end
context 'admin notes' do context 'admin notes' do
......
...@@ -142,8 +142,8 @@ RSpec.describe Gitlab::Middleware::Go do ...@@ -142,8 +142,8 @@ RSpec.describe Gitlab::Middleware::Go do
response = go response = go
expect(response[0]).to eq(403) expect(response[0]).to eq(403)
expect(response[1]['Content-Length']).to eq('0') expect(response[1]['Content-Length']).to be_nil
expect(response[2].body).to eq(['']) expect(response[2]).to eq([''])
end end
end end
end end
...@@ -187,10 +187,11 @@ RSpec.describe Gitlab::Middleware::Go do ...@@ -187,10 +187,11 @@ RSpec.describe Gitlab::Middleware::Go do
it 'returns 404' do it 'returns 404' do
response = go response = go
expect(response[0]).to eq(404) expect(response[0]).to eq(404)
expect(response[1]['Content-Type']).to eq('text/html') expect(response[1]['Content-Type']).to eq('text/html')
expected_body = %{<html><body>go get #{Gitlab.config.gitlab.url}/#{project.full_path}</body></html>} expected_body = %{<html><body>go get #{Gitlab.config.gitlab.url}/#{project.full_path}</body></html>}
expect(response[2].body).to eq([expected_body]) expect(response[2]).to eq([expected_body])
end end
end end
...@@ -262,7 +263,7 @@ RSpec.describe Gitlab::Middleware::Go do ...@@ -262,7 +263,7 @@ RSpec.describe Gitlab::Middleware::Go do
expect(response[0]).to eq(200) expect(response[0]).to eq(200)
expect(response[1]['Content-Type']).to eq('text/html') expect(response[1]['Content-Type']).to eq('text/html')
expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}" /><meta name="go-source" content="#{Gitlab.config.gitlab.host}/#{path} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}" /></head><body>go get #{Gitlab.config.gitlab.url}/#{path}</body></html>} expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}" /><meta name="go-source" content="#{Gitlab.config.gitlab.host}/#{path} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}" /></head><body>go get #{Gitlab.config.gitlab.url}/#{path}</body></html>}
expect(response[2].body).to eq([expected_body]) expect(response[2]).to eq([expected_body])
end end
end end
end end
...@@ -60,12 +60,12 @@ RSpec.describe Gitlab::Middleware::SameSiteCookies do ...@@ -60,12 +60,12 @@ RSpec.describe Gitlab::Middleware::SameSiteCookies do
end end
context 'with no cookies' do context 'with no cookies' do
let(:cookies) { nil } let(:cookies) { "" }
it 'does not add headers' do it 'does not add headers' do
response = do_request response = do_request
expect(response['Set-Cookie']).to be_nil expect(response['Set-Cookie']).to eq("")
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::APIGuard::ResponseCoercerMiddleware do
using RSpec::Parameterized::TableSyntax
it 'is loaded' do
expect(API::API.middleware).to include([:use, described_class])
end
describe '#call' do
let(:app) do
Class.new(API::API)
end
[
nil, 201, 10.5, "test"
].each do |val|
it 'returns a String body' do
app.get 'bodytest' do
status 200
env['api.format'] = :binary
body val
end
unless val.is_a?(String)
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(instance_of(ArgumentError))
end
get api('/bodytest')
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq(val.to_s)
end
end
[100, 204, 304].each do |status|
it 'allows nil body' do
app.get 'statustest' do
status status
env['api.format'] = :binary
body nil
end
expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
get api('/statustest')
expect(response.status).to eq(status)
expect(response.body).to eq('')
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment