Commit abc34b9d authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '257822-null-byes-in-url' into 'master'

Expand HandleNullBytes to handle malformed strings

See merge request gitlab-org/gitlab!45701
parents 9021c154 fb77e461
---
title: Handle malformed strings in URL
merge_request: 45701
author:
type: fixed
...@@ -28,7 +28,7 @@ module Gitlab ...@@ -28,7 +28,7 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/middleware/basic_health_check') require_dependency Rails.root.join('lib/gitlab/middleware/basic_health_check')
require_dependency Rails.root.join('lib/gitlab/middleware/same_site_cookies') require_dependency Rails.root.join('lib/gitlab/middleware/same_site_cookies')
require_dependency Rails.root.join('lib/gitlab/middleware/handle_ip_spoof_attack_error') require_dependency Rails.root.join('lib/gitlab/middleware/handle_ip_spoof_attack_error')
require_dependency Rails.root.join('lib/gitlab/middleware/handle_null_bytes') require_dependency Rails.root.join('lib/gitlab/middleware/handle_malformed_strings')
require_dependency Rails.root.join('lib/gitlab/runtime') require_dependency Rails.root.join('lib/gitlab/runtime')
# Settings in config/environments/* take precedence over those specified here. # Settings in config/environments/* take precedence over those specified here.
...@@ -257,7 +257,7 @@ module Gitlab ...@@ -257,7 +257,7 @@ module Gitlab
config.middleware.insert_before ActionDispatch::RemoteIp, ::Gitlab::Middleware::HandleIpSpoofAttackError config.middleware.insert_before ActionDispatch::RemoteIp, ::Gitlab::Middleware::HandleIpSpoofAttackError
config.middleware.use ::Gitlab::Middleware::HandleNullBytes config.middleware.insert_after ActionDispatch::ActionableExceptions, ::Gitlab::Middleware::HandleMalformedStrings
# Allow access to GitLab API from other domains # Allow access to GitLab API from other domains
config.middleware.insert_before Warden::Manager, Rack::Cors do config.middleware.insert_before Warden::Manager, Rack::Cors do
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
module Gitlab module Gitlab
module Middleware module Middleware
# There is no valid reason for a request to contain a null byte (U+0000) # There is no valid reason for a request to contain a malformed string
# so just return HTTP 400 (Bad Request) if we receive one # so just return HTTP 400 (Bad Request) if we receive one
class HandleNullBytes class HandleMalformedStrings
NULL_BYTE_REGEX = Regexp.new(Regexp.escape("\u0000")).freeze NULL_BYTE_REGEX = Regexp.new(Regexp.escape("\u0000")).freeze
attr_reader :app attr_reader :app
...@@ -14,18 +14,20 @@ module Gitlab ...@@ -14,18 +14,20 @@ module Gitlab
end end
def call(env) def call(env)
return [400, {}, ["Bad Request"]] if request_has_null_byte?(env) return [400, { 'Content-Type' => 'text/plain' }, ['Bad Request']] if request_contains_malformed_string?(env)
app.call(env) app.call(env)
end end
private private
def request_has_null_byte?(request) def request_contains_malformed_string?(request)
return false if ENV['REJECT_NULL_BYTES'] == "1" return false if ENV['DISABLE_REQUEST_VALIDATION'] == '1'
request = Rack::Request.new(request) request = Rack::Request.new(request)
return true if string_malformed?(request.path)
request.params.values.any? do |value| request.params.values.any? do |value|
param_has_null_byte?(value) param_has_null_byte?(value)
end end
...@@ -39,7 +41,7 @@ module Gitlab ...@@ -39,7 +41,7 @@ module Gitlab
depth += 1 depth += 1
if value.respond_to?(:match) if value.respond_to?(:match)
string_contains_null_byte?(value) string_malformed?(value)
elsif value.respond_to?(:values) elsif value.respond_to?(:values)
value.values.any? do |hash_value| value.values.any? do |hash_value|
param_has_null_byte?(hash_value, depth) param_has_null_byte?(hash_value, depth)
...@@ -53,8 +55,11 @@ module Gitlab ...@@ -53,8 +55,11 @@ module Gitlab
end end
end end
def string_contains_null_byte?(string) def string_malformed?(string)
string.match?(NULL_BYTE_REGEX) string.match?(NULL_BYTE_REGEX)
rescue ArgumentError
# If we're here, we caught a malformed string. Return true
true
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
require "rack/test"
RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
let(:null_byte) { "\u0000" }
let(:invalid_string) { "mal\xC0formed" }
let(:error_400) { [400, { 'Content-Type' => 'text/plain' }, ['Bad Request']] }
let(:app) { double(:app) }
subject { described_class.new(app) }
before do
allow(app).to receive(:call) do |args|
args
end
end
def env_for(params = {})
Rack::MockRequest.env_for('/', { params: params })
end
context 'in the URL' do
it 'rejects null bytes' do
# We have to create the env separately or Rack::MockRequest complains about invalid URI
env = env_for
env['PATH_INFO'] = "/someplace/witha#{null_byte}nullbyte"
expect(subject.call(env)).to eq error_400
end
it 'rejects malformed strings' do
# We have to create the env separately or Rack::MockRequest complains about invalid URI
env = env_for
env['PATH_INFO'] = "/someplace/with_an/#{invalid_string}"
expect(subject.call(env)).to eq error_400
end
end
context 'in params' do
shared_examples_for 'checks params' do
it 'rejects bad params in a top level param' do
env = env_for(name: "null#{problematic_input}byte")
expect(subject.call(env)).to eq error_400
end
it "rejects bad params for hashes with strings" do
env = env_for(name: { inner_key: "I am #{problematic_input} bad" })
expect(subject.call(env)).to eq error_400
end
it "rejects bad params for arrays with strings" do
env = env_for(name: ["I am #{problematic_input} bad"])
expect(subject.call(env)).to eq error_400
end
it "rejects bad params for arrays containing hashes with string values" do
env = env_for(name: [
{
inner_key: "I am #{problematic_input} bad"
}
])
expect(subject.call(env)).to eq error_400
end
it "gives up and does not reject too deeply nested params" do
env = env_for(name: [
{
inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{problematic_input} bad" }] }
}
])
expect(subject.call(env)).not_to eq error_400
end
end
context 'with null byte' do
it_behaves_like 'checks params' do
let(:problematic_input) { null_byte }
end
end
context 'with malformed strings' do
it_behaves_like 'checks params' do
let(:problematic_input) { invalid_string }
end
end
end
context 'without problematic input' do
it "does not error for strings" do
env = env_for(name: "safe name")
expect(subject.call(env)).not_to eq error_400
end
it "does not error with no params" do
env = env_for
expect(subject.call(env)).not_to eq error_400
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require "rack/test"
RSpec.describe Gitlab::Middleware::HandleNullBytes do
let(:null_byte) { "\u0000" }
let(:error_400) { [400, {}, ["Bad Request"]] }
let(:app) { double(:app) }
subject { described_class.new(app) }
before do
allow(app).to receive(:call) do |args|
args
end
end
def env_for(params = {})
Rack::MockRequest.env_for('/', { params: params })
end
context 'with null bytes in params' do
it 'rejects null bytes in a top level param' do
env = env_for(name: "null#{null_byte}byte")
expect(subject.call(env)).to eq error_400
end
it "responds with 400 BadRequest for hashes with strings" do
env = env_for(name: { inner_key: "I am #{null_byte} bad" })
expect(subject.call(env)).to eq error_400
end
it "responds with 400 BadRequest for arrays with strings" do
env = env_for(name: ["I am #{null_byte} bad"])
expect(subject.call(env)).to eq error_400
end
it "responds with 400 BadRequest for arrays containing hashes with string values" do
env = env_for(name: [
{
inner_key: "I am #{null_byte} bad"
}
])
expect(subject.call(env)).to eq error_400
end
it "gives up and does not 400 with too deeply nested params" do
env = env_for(name: [
{
inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{null_byte} bad" }] }
}
])
expect(subject.call(env)).not_to eq error_400
end
end
context 'without null bytes in params' do
it "does not respond with a 400 for strings" do
env = env_for(name: "safe name")
expect(subject.call(env)).not_to eq error_400
end
it "does not respond with a 400 with no params" do
env = env_for
expect(subject.call(env)).not_to eq error_400
end
end
context 'when disabled via env flag' do
before do
stub_env('REJECT_NULL_BYTES', '1')
end
it 'does not respond with a 400 no matter what' do
env = env_for(name: "null#{null_byte}byte")
expect(subject.call(env)).not_to eq error_400
end
end
end
...@@ -2,13 +2,19 @@ ...@@ -2,13 +2,19 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'User sends null bytes as params' do RSpec.describe 'User sends malformed strings as params' do
let(:null_byte) { "\u0000" } let(:null_byte) { "\u0000" }
let(:invalid_string) { "mal\xC0formed" }
it 'raises a 400 error' do it 'raises a 400 error with a null byte' do
post '/nonexistent', params: { a: "A #{null_byte} nasty string" } post '/nonexistent', params: { a: "A #{null_byte} nasty string" }
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to eq('Bad Request') end
it 'raises a 400 error with an invalid string' do
post '/nonexistent', params: { a: "A #{invalid_string} nasty string" }
expect(response).to have_gitlab_http_status(:bad_request)
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